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:
2025-09-08 18:11:36 +00:00
committed by Tomek Wójcik
parent ffecf780ee
commit b6d02dbe78
184 changed files with 7536 additions and 163 deletions

View 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`);
};