You've already forked hotpocket
BTHLABS-50: Safari Web extension
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl> Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
This commit is contained in:
300
services/extension/src/background/main.js
Normal file
300
services/extension/src/background/main.js
Normal file
@@ -0,0 +1,300 @@
|
||||
import HotPocketExtension from '../common';
|
||||
|
||||
const AUTH_URL = (new URL('/integrations/extension/authenticate/', HotPocketExtension.base_url)).toString();
|
||||
const POST_AUTH_URL = (new URL('/integrations/extension/post-authenticate/', HotPocketExtension.base_url)).toString();
|
||||
const RPC_URL = (new URL('/rpc/', HotPocketExtension.base_url)).toString();
|
||||
|
||||
const makeJSONRPCCall = (method, params) => {
|
||||
return {
|
||||
'jsonrpc': '2.0',
|
||||
'id': (new Date().toISOString()),
|
||||
'method': method,
|
||||
'params': params,
|
||||
};
|
||||
};
|
||||
|
||||
const executeJSONRPCCall = async (url, call, {accessToken}) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.background.executeJSONRPCCall()', url, call, accessToken,
|
||||
);
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (accessToken) {
|
||||
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
let result = null;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
url,
|
||||
{
|
||||
body: JSON.stringify(call),
|
||||
credentials: 'include',
|
||||
headers: headers,
|
||||
method: 'POST',
|
||||
},
|
||||
);
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.executeJSONRPCCall()', response,
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
error = {
|
||||
code: -32000,
|
||||
message: 'Fetch error',
|
||||
data: response,
|
||||
};
|
||||
} else {
|
||||
const callResult = await response.json();
|
||||
if (callResult.error) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.content.executeJSONRPCCall(): RPC error',
|
||||
callResult.error.code,
|
||||
callResult.error.message,
|
||||
callResult.error.data,
|
||||
);
|
||||
|
||||
error = callResult.error;
|
||||
} else {
|
||||
result = callResult.result;
|
||||
}
|
||||
}
|
||||
} catch (exception) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.content.executeJSONRPCCall(): Fetch error', exception,
|
||||
);
|
||||
error = {
|
||||
code: -32000,
|
||||
message: 'Fetch error',
|
||||
data: exception,
|
||||
};
|
||||
}
|
||||
|
||||
return [result, error];
|
||||
};
|
||||
|
||||
const doSave = async (accessToken, tab) => {
|
||||
const call = makeJSONRPCCall('saves.create', [tab.url]);
|
||||
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {accessToken});
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.doSave():', result, error,
|
||||
);
|
||||
|
||||
if (error !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const doCreateAndStoreAccessToken = async (authKey) => {
|
||||
const accessTokenCall = makeJSONRPCCall(
|
||||
'accounts.access_tokens.create',
|
||||
[
|
||||
authKey,
|
||||
{
|
||||
platform: navigator.platform,
|
||||
version: HotPocketExtension.version,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const [accessToken, error] = await executeJSONRPCCall(
|
||||
RPC_URL, accessTokenCall, {accessToken: null},
|
||||
);
|
||||
|
||||
if (error === null) {
|
||||
await HotPocketExtension.api.storage.local.set({
|
||||
accessToken: accessToken,
|
||||
});
|
||||
}
|
||||
|
||||
return [accessToken, error];
|
||||
};
|
||||
|
||||
const doHandleAuthFlow = (authTab) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onTabsUpdated = (tabId, changeInfo, updatedTab) => {
|
||||
if (tabId === authTab.id) {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.doHandleAuthFlow.onTabsUpdated()', updatedTab, changeInfo,
|
||||
);
|
||||
|
||||
const changedURL = changeInfo.url;
|
||||
if (changedURL && changedURL.startsWith(POST_AUTH_URL)) {
|
||||
const parsedChangedURL = new URL(changedURL);
|
||||
const authKey = parsedChangedURL.searchParams.get('auth_key');
|
||||
|
||||
doCreateAndStoreAccessToken(authKey).
|
||||
then((result) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'doHandleAuthFlow.onTabsUpdated.doGetAndStoreAccessToken.then()', result,
|
||||
);
|
||||
const [accessToken, error] = result;
|
||||
|
||||
if (error !== null) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(accessToken);
|
||||
}
|
||||
}).
|
||||
catch((error) => {
|
||||
reject(error);
|
||||
}).
|
||||
finally(() => {
|
||||
HotPocketExtension.api.tabs.onUpdated.removeListener(onTabsUpdated);
|
||||
HotPocketExtension.api.tabs.remove(authTab.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
HotPocketExtension.api.tabs.onUpdated.addListener(onTabsUpdated);
|
||||
});
|
||||
};
|
||||
|
||||
const doCheckAuth = async (accessToken) => {
|
||||
if (accessToken === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const call = makeJSONRPCCall('accounts.auth.check');
|
||||
|
||||
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {
|
||||
accessToken,
|
||||
});
|
||||
|
||||
if (error !== null) {
|
||||
if (error.data instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error.data.status === 403) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
};
|
||||
|
||||
const doGetAccessToken = async () => {
|
||||
let storageResult = await HotPocketExtension.api.storage.local.get('accessToken');
|
||||
let accessToken = await doCheckAuth(
|
||||
storageResult.accessToken || null,
|
||||
);
|
||||
|
||||
if (accessToken === null) {
|
||||
const authTab = await HotPocketExtension.api.tabs.create({
|
||||
url: AUTH_URL,
|
||||
});
|
||||
|
||||
accessToken = await doHandleAuthFlow(authTab);
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
};
|
||||
|
||||
const doSendTabMessage = (tab, message) => {
|
||||
HotPocketExtension.api.tabs.sendMessage(tab.id, message).
|
||||
then((result) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.doSendTabMessage(): message sent', message, result,
|
||||
);
|
||||
}).
|
||||
catch((error) => {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.content.doSendTabMessage(): could not send message', error,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onTabCreated = (tab) => {
|
||||
HotPocketExtension.LOGGER.debug('HotPocketExtension.onTabCreated()', tab);
|
||||
HotPocketExtension.api.action.enable(tab.id);
|
||||
};
|
||||
|
||||
const onBrowserActionClicked = async (tab) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.onBrowserActionClicked()', tab.url,
|
||||
);
|
||||
|
||||
if (!tab.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
let accessToken = await doGetAccessToken();
|
||||
|
||||
result = await doSave(accessToken, tab);
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.onBrowserActionClicked()', result,
|
||||
);
|
||||
} catch (exception) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'Unhandled exception when handling action click',
|
||||
exception,
|
||||
);
|
||||
error = exception;
|
||||
}
|
||||
|
||||
const message = {
|
||||
type: 'HotPocket:Extension:save',
|
||||
result: result,
|
||||
error: error,
|
||||
};
|
||||
|
||||
doSendTabMessage(tab, message);
|
||||
};
|
||||
|
||||
const onMessage = (message, sender, sendResponse) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.background.onMessage()', message, sender, sendResponse,
|
||||
);
|
||||
|
||||
let response = {ok: true};
|
||||
try {
|
||||
if (message.type === 'HotPocket:Extension:ping') {
|
||||
HotPocketExtension.LOGGER.debug(sender.tab.id);
|
||||
doSendTabMessage(sender.tab, {
|
||||
type: 'HotPocket:Extension:pong',
|
||||
});
|
||||
}
|
||||
} catch (exception) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.background.onMessage(): Unhandled exception when handling content message',
|
||||
message,
|
||||
exception,
|
||||
);
|
||||
response.ok = false;
|
||||
}
|
||||
|
||||
sendResponse(response);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default ({...configuration}) => {
|
||||
HotPocketExtension.configure(configuration, {
|
||||
background: true,
|
||||
});
|
||||
|
||||
HotPocketExtension.api.tabs.onCreated.addListener(onTabCreated);
|
||||
|
||||
HotPocketExtension.api.action.onClicked.addListener(onBrowserActionClicked);
|
||||
|
||||
HotPocketExtension.api.runtime.onMessage.addListener(onMessage);
|
||||
|
||||
HotPocketExtension.LOGGER.info(`HotPocket v${HotPocketExtension.version} by BTHLabs`);
|
||||
};
|
||||
6
services/extension/src/background/safari.js
Normal file
6
services/extension/src/background/safari.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import main from './main';
|
||||
|
||||
main({
|
||||
platform: 'Safari',
|
||||
api: browser,
|
||||
});
|
||||
34
services/extension/src/common.js
Normal file
34
services/extension/src/common.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const ENV = __HOTPOCKET_EXTENSION_ENV__;
|
||||
const DEBUG = (ENV === 'development') ? true : false;
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const HotPocketExtension = {
|
||||
platform: null,
|
||||
version: __HOTPOCKET_EXTENSION_VERSION__,
|
||||
debug: DEBUG,
|
||||
api: null,
|
||||
base_url: __HOTPOCKET_EXTENSION_BASE_URL__,
|
||||
LOGGER: {
|
||||
// eslint-disable-next-line no-console
|
||||
debug: (DEBUG === true) ? console.log : noop,
|
||||
error: console.error,
|
||||
info: console.info,
|
||||
warning: console.warn,
|
||||
},
|
||||
configure: ({platform, debug, api}, options) => {
|
||||
options = options || {};
|
||||
|
||||
HotPocketExtension.platform = platform;
|
||||
HotPocketExtension.debug = debug;
|
||||
HotPocketExtension.api = api;
|
||||
|
||||
const background = (options.background === true) ? true : false;
|
||||
|
||||
if (background === false && HotPocketExtension.debug === true) {
|
||||
window.HotPocketExtension = HotPocketExtension;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default HotPocketExtension;
|
||||
118
services/extension/src/content/main.js
Normal file
118
services/extension/src/content/main.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import HotPocketExtension from '../common';
|
||||
|
||||
import POPUP from './templates/popup.html';
|
||||
import POPUP_CONTENT_SUCCESS from './templates/popup_content_success.html';
|
||||
import POPUP_CONTENT_ERROR from './templates/popup_content_error.html';
|
||||
|
||||
class Popup {
|
||||
constructor () {
|
||||
this.container = null;
|
||||
this.timeout = null;
|
||||
}
|
||||
setCloseTimeout = () => {
|
||||
this.timeout = window.setTimeout(this.close, 5000);
|
||||
};
|
||||
clearCloseTimeout = () => {
|
||||
if (this.timeout !== null) {
|
||||
window.clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
};
|
||||
close = () => {
|
||||
this.clearCloseTimeout();
|
||||
|
||||
if (this.container !== null) {
|
||||
this.container.remove();
|
||||
this.container = null;
|
||||
}
|
||||
};
|
||||
show = (content) => {
|
||||
this.close();
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.container.id = 'hotpocket-extension-popup';
|
||||
this.container.hotPocketExtensionPopup = this;
|
||||
|
||||
const shadow = this.container.attachShadow({mode: 'open'});
|
||||
shadow.innerHTML = POPUP;
|
||||
|
||||
const body = shadow.querySelector('.hotpocket-extension-popup-body');
|
||||
body.innerHTML = content;
|
||||
|
||||
const i18nElements = shadow.querySelectorAll('[data-message]');
|
||||
for (let i18nElement of i18nElements) {
|
||||
i18nElement.innerHTML = HotPocketExtension.api.i18n.getMessage(
|
||||
i18nElement.dataset.message,
|
||||
);
|
||||
}
|
||||
|
||||
const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close');
|
||||
for (const closeElement of closeElements) {
|
||||
closeElement.addEventListener('click', this.onCloseClick);
|
||||
}
|
||||
|
||||
document.body.appendChild(this.container);
|
||||
};
|
||||
onCloseClick = (event) => {
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
let currentPopup = null;
|
||||
|
||||
const doHandleSaveMessage = (message) => {
|
||||
if (currentPopup !== null) {
|
||||
currentPopup.close();
|
||||
}
|
||||
|
||||
currentPopup = new Popup();
|
||||
currentPopup.show(
|
||||
(message.result === true) ? POPUP_CONTENT_SUCCESS : POPUP_CONTENT_ERROR,
|
||||
);
|
||||
};
|
||||
|
||||
const doSendMessage = (message) => {
|
||||
HotPocketExtension.api.runtime.sendMessage(message).
|
||||
then((result) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.doSendMessage(): message sent', message, result,
|
||||
);
|
||||
}).
|
||||
catch((error) => {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.content.doSendMessage(): could not send message', error,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default ({...configuration}) => {
|
||||
HotPocketExtension.configure(configuration);
|
||||
|
||||
HotPocketExtension.api.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
HotPocketExtension.LOGGER.debug('HotPocketExtension.content.onMessage()', message, sender, sendResponse);
|
||||
|
||||
let response = {ok: true};
|
||||
try {
|
||||
if (message.type === 'HotPocket:Extension:save') {
|
||||
doHandleSaveMessage(message);
|
||||
}
|
||||
} catch (exception) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.content.onMessage(): Unhandled exception when handling service worker message',
|
||||
message,
|
||||
exception,
|
||||
);
|
||||
response.ok = false;
|
||||
}
|
||||
|
||||
sendResponse(response);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
doSendMessage({
|
||||
type: 'HotPocket:Extension:ping',
|
||||
});
|
||||
|
||||
HotPocketExtension.LOGGER.info(`HotPocket v${HotPocketExtension.version} by BTHLabs`);
|
||||
};
|
||||
6
services/extension/src/content/safari.js
Normal file
6
services/extension/src/content/safari.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import main from './main';
|
||||
|
||||
main({
|
||||
platform: 'Safari',
|
||||
api: window.browser,
|
||||
});
|
||||
64
services/extension/src/content/templates/popup.html
Normal file
64
services/extension/src/content/templates/popup.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<style type="text/css">
|
||||
:host {
|
||||
all: initial;
|
||||
font-family: sans-serif !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.5 !important;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 999999;
|
||||
}
|
||||
.hotpocket-extension-popup {
|
||||
background: #212529;
|
||||
border: 1px solid #495057;
|
||||
border-radius: 0.375rem;
|
||||
color: white;
|
||||
width: 300px;
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-close {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-header {
|
||||
background: rgba(222, 226, 230, 0.3);
|
||||
border-bottom: 1px solid #495057;
|
||||
padding: 0.5rem 1rem;
|
||||
position: relative;
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-header strong {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-header .hotpocket-extension-popup-close {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
text-align: center;
|
||||
top: 0.5rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-body > * {
|
||||
margin: 0px;
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-message-success {
|
||||
color: rgba(25, 135, 84, 1);
|
||||
}
|
||||
.hotpocket-extension-popup .hotpocket-extension-popup-message-error {
|
||||
color: rgba(220, 53, 69, 1);
|
||||
}
|
||||
</style>
|
||||
<div class="hotpocket-extension-popup">
|
||||
<div class="hotpocket-extension-popup-header">
|
||||
<strong>HotPocket by BTHLabs</strong>
|
||||
<a class="hotpocket-extension-popup-close">×</a>
|
||||
</div>
|
||||
<div class="hotpocket-extension-popup-body">
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<p class="hotpocket-extension-popup-message hotpocket-extension-popup-message-error">
|
||||
<strong data-message="content_popup_content_error_title"></strong>
|
||||
<br>
|
||||
<span data-message="content_popup_content_error_message"></span>
|
||||
</p>
|
||||
@@ -0,0 +1,5 @@
|
||||
<p class="hotpocket-extension-popup-message hotpocket-extension-popup-message-success">
|
||||
<strong data-message="content_popup_content_success_title"></strong>
|
||||
<br>
|
||||
<span data-message="content_popup_content_success_message"></span>
|
||||
</p>
|
||||
1
services/extension/src/manifest.js
Normal file
1
services/extension/src/manifest.js
Normal file
@@ -0,0 +1 @@
|
||||
export default 'This file intentionally left blank';
|
||||
34
services/extension/src/manifest/common.json
Normal file
34
services/extension/src/manifest/common.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"default_locale": "en",
|
||||
"name": "__MSG_extension_name__",
|
||||
"description": "__MSG_extension_description__",
|
||||
"version": "1.0",
|
||||
"icons": {
|
||||
"48": "images/icon-48.png",
|
||||
"64": "images/icon-64.png",
|
||||
"96": "images/icon-96.png",
|
||||
"128": "images/icon-128.png",
|
||||
"256": "images/icon-256.png",
|
||||
"512": "images/icon-512.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": [
|
||||
"content-bundle.js"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_title": "__MSG_extension_name__",
|
||||
"default_icon": "images/toolbar-icon.svg"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"tabs"
|
||||
]
|
||||
}
|
||||
9
services/extension/src/manifest/safari.json
Normal file
9
services/extension/src/manifest/safari.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"description": "__MSG_extension_description_Safari__",
|
||||
"background": {
|
||||
"scripts": [
|
||||
"background-bundle.js"
|
||||
],
|
||||
"type": "module"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user