mirror of https://github.com/dessant/buster
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
515 lines
11 KiB
JavaScript
515 lines
11 KiB
JavaScript
import {v4 as uuidv4} from 'uuid';
|
|
|
|
import storage from 'storage/storage';
|
|
import {
|
|
getText,
|
|
insertCSS,
|
|
executeScript,
|
|
createTab,
|
|
getActiveTab,
|
|
isValidTab,
|
|
getPlatform,
|
|
getDayPrecisionEpoch,
|
|
getDarkColorSchemeQuery,
|
|
getRandomInt,
|
|
sleep
|
|
} from 'utils/common';
|
|
import {recaptchaChallengeUrlRx} from 'utils/data';
|
|
import {
|
|
targetEnv,
|
|
enableContributions,
|
|
storageRevisions,
|
|
appVersion,
|
|
mv3
|
|
} from 'utils/config';
|
|
|
|
async function showNotification({
|
|
message,
|
|
messageId,
|
|
title,
|
|
type = 'info',
|
|
timeout = 0
|
|
} = {}) {
|
|
if (!title) {
|
|
title = getText('extensionName');
|
|
}
|
|
if (messageId) {
|
|
message = getText(messageId);
|
|
}
|
|
|
|
if (targetEnv === 'safari') {
|
|
return browser.runtime.sendNativeMessage('application.id', {
|
|
id: 'notification',
|
|
message
|
|
});
|
|
} else {
|
|
const notification = await browser.notifications.create(
|
|
`bc-notification-${type}`,
|
|
{
|
|
type: 'basic',
|
|
title,
|
|
message,
|
|
iconUrl: '/src/assets/icons/app/icon-64.png'
|
|
}
|
|
);
|
|
|
|
if (timeout) {
|
|
self.setTimeout(() => {
|
|
browser.notifications.clear(notification);
|
|
}, timeout);
|
|
}
|
|
|
|
return notification;
|
|
}
|
|
}
|
|
|
|
function getListItems(data, {scope = '', shortScope = ''} = {}) {
|
|
const results = {};
|
|
|
|
for (const [group, items] of Object.entries(data)) {
|
|
results[group] = [];
|
|
|
|
items.forEach(function (item) {
|
|
if (item.value === undefined) {
|
|
item = {value: item};
|
|
}
|
|
|
|
item.title = getText(`${scope ? scope + '_' : ''}${item.value}`);
|
|
|
|
if (shortScope) {
|
|
item.shortTitle = getText(`${shortScope}_${item.value}`);
|
|
}
|
|
|
|
results[group].push(item);
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async function insertBaseModule({activeTab = false} = {}) {
|
|
const tabs = [];
|
|
if (activeTab) {
|
|
const tab = await getActiveTab();
|
|
if (tab) {
|
|
tabs.push(tab);
|
|
}
|
|
} else {
|
|
tabs.push(
|
|
...(await browser.tabs.query({
|
|
url: ['http://*/*', 'https://*/*'],
|
|
windowType: 'normal'
|
|
}))
|
|
);
|
|
}
|
|
|
|
for (const tab of tabs) {
|
|
const tabId = tab.id;
|
|
|
|
const frames = await browser.webNavigation.getAllFrames({tabId});
|
|
for (const frame of frames) {
|
|
const frameId = frame.frameId;
|
|
|
|
if (frameId && recaptchaChallengeUrlRx.test(frame.url)) {
|
|
insertCSS({
|
|
files: ['/src/base/style.css'],
|
|
tabId,
|
|
frameIds: [frameId]
|
|
});
|
|
|
|
executeScript({
|
|
files: ['/src/base/script.js'],
|
|
tabId,
|
|
frameIds: [frameId],
|
|
injectImmediately: false
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadFonts(fonts) {
|
|
await Promise.allSettled(fonts.map(font => document.fonts.load(font)));
|
|
}
|
|
|
|
function processMessageResponse(response, sendResponse) {
|
|
if (targetEnv === 'safari') {
|
|
response.then(function (result) {
|
|
// Safari 15: undefined response will cause sendMessage to never resolve.
|
|
if (result === undefined) {
|
|
result = null;
|
|
}
|
|
sendResponse(result);
|
|
});
|
|
|
|
return true;
|
|
} else {
|
|
return response;
|
|
}
|
|
}
|
|
|
|
async function configApp(app) {
|
|
const platform = await getPlatform();
|
|
|
|
const classes = [platform.targetEnv, platform.os];
|
|
document.documentElement.classList.add(...classes);
|
|
|
|
if (app) {
|
|
app.config.globalProperties.$env = platform;
|
|
}
|
|
}
|
|
|
|
async function getAppTheme(theme) {
|
|
if (!theme) {
|
|
({appTheme: theme} = await storage.get('appTheme'));
|
|
}
|
|
|
|
if (theme === 'auto') {
|
|
theme = getDarkColorSchemeQuery().matches ? 'dark' : 'light';
|
|
}
|
|
|
|
return theme;
|
|
}
|
|
|
|
function addSystemThemeListener(callback) {
|
|
getDarkColorSchemeQuery().addEventListener('change', function () {
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function addAppThemeListener(callback) {
|
|
browser.storage.onChanged.addListener(function (changes, area) {
|
|
if (area === 'local' && changes.appTheme) {
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
|
|
function addThemeListener(callback) {
|
|
addSystemThemeListener(callback);
|
|
addAppThemeListener(callback);
|
|
}
|
|
|
|
async function getOpenerTabId({tab, tabId = null} = {}) {
|
|
if (!tab && tabId !== null) {
|
|
tab = await browser.tabs.get(tabId).catch(err => null);
|
|
}
|
|
|
|
if ((await isValidTab({tab})) && !(await getPlatform()).isMobile) {
|
|
return tab.id;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function showPage({
|
|
url = '',
|
|
setOpenerTab = true,
|
|
getTab = false,
|
|
activeTab = null
|
|
} = {}) {
|
|
if (!activeTab) {
|
|
activeTab = await getActiveTab();
|
|
}
|
|
|
|
const props = {url, index: activeTab.index + 1, active: true, getTab};
|
|
|
|
if (setOpenerTab) {
|
|
props.openerTabId = await getOpenerTabId({tab: activeTab});
|
|
}
|
|
|
|
return createTab(props);
|
|
}
|
|
|
|
async function autoShowContributePage({
|
|
minUseCount = 0, // 0-1000
|
|
minInstallDays = 0,
|
|
minLastOpenDays = 0,
|
|
minLastAutoOpenDays = 0,
|
|
action = 'auto',
|
|
activeTab = null
|
|
} = {}) {
|
|
if (enableContributions) {
|
|
const options = await storage.get([
|
|
'showContribPage',
|
|
'useCount',
|
|
'installTime',
|
|
'contribPageLastOpen',
|
|
'contribPageLastAutoOpen'
|
|
]);
|
|
|
|
const epoch = getDayPrecisionEpoch();
|
|
|
|
if (
|
|
options.showContribPage &&
|
|
options.useCount >= minUseCount &&
|
|
epoch - options.installTime >= minInstallDays * 86400000 &&
|
|
epoch - options.contribPageLastOpen >= minLastOpenDays * 86400000 &&
|
|
epoch - options.contribPageLastAutoOpen >= minLastAutoOpenDays * 86400000
|
|
) {
|
|
await storage.set({
|
|
contribPageLastOpen: epoch,
|
|
contribPageLastAutoOpen: epoch
|
|
});
|
|
|
|
return showContributePage({
|
|
action,
|
|
updateStats: false,
|
|
activeTab,
|
|
getTab: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let useCountLastUpdate = 0;
|
|
async function updateUseCount({
|
|
valueChange = 1,
|
|
maxUseCount = Infinity,
|
|
minInterval = 0
|
|
} = {}) {
|
|
if (Date.now() - useCountLastUpdate >= minInterval) {
|
|
useCountLastUpdate = Date.now();
|
|
|
|
const {useCount} = await storage.get('useCount');
|
|
|
|
if (useCount < maxUseCount) {
|
|
await storage.set({useCount: useCount + valueChange});
|
|
} else if (useCount > maxUseCount) {
|
|
await storage.set({useCount: maxUseCount});
|
|
}
|
|
}
|
|
}
|
|
|
|
async function processAppUse({
|
|
action = 'auto',
|
|
activeTab = null,
|
|
showContribPage = true
|
|
} = {}) {
|
|
await updateUseCount({
|
|
valueChange: 1,
|
|
maxUseCount: 1000
|
|
});
|
|
|
|
if (showContribPage) {
|
|
return autoShowContributePage({
|
|
minUseCount: 10,
|
|
minInstallDays: 14,
|
|
minLastOpenDays: 14,
|
|
minLastAutoOpenDays: 365,
|
|
activeTab,
|
|
action
|
|
});
|
|
}
|
|
}
|
|
|
|
async function showContributePage({
|
|
action = '',
|
|
updateStats = true,
|
|
getTab = false,
|
|
activeTab = null
|
|
} = {}) {
|
|
if (updateStats) {
|
|
await storage.set({contribPageLastOpen: getDayPrecisionEpoch()});
|
|
}
|
|
|
|
let url = browser.runtime.getURL('/src/contribute/index.html');
|
|
if (action) {
|
|
url = `${url}?action=${action}`;
|
|
}
|
|
|
|
return showPage({url, getTab, activeTab});
|
|
}
|
|
|
|
async function showOptionsPage({getTab = false, activeTab = null} = {}) {
|
|
// Samsung Internet 13: runtime.openOptionsPage fails.
|
|
// runtime.openOptionsPage adds new tab at the end of the tab list.
|
|
return showPage({
|
|
url: browser.runtime.getURL('/src/options/index.html'),
|
|
getTab,
|
|
activeTab
|
|
});
|
|
}
|
|
|
|
async function setAppVersion() {
|
|
await storage.set({appVersion});
|
|
}
|
|
|
|
async function isSessionStartup() {
|
|
const privateContext = browser.extension.inIncognitoContext;
|
|
|
|
const sessionKey = privateContext ? 'privateSession' : 'session';
|
|
const session = (await browser.storage.session.get(sessionKey))[sessionKey];
|
|
|
|
if (!session) {
|
|
await browser.storage.session.set({[sessionKey]: true});
|
|
}
|
|
|
|
if (privateContext) {
|
|
try {
|
|
if (!(await self.caches.has(sessionKey))) {
|
|
await self.caches.open(sessionKey);
|
|
|
|
return true;
|
|
}
|
|
} catch (err) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!session) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
async function isStartup() {
|
|
const startup = {
|
|
install: false,
|
|
update: false,
|
|
session: false,
|
|
setupInstance: false,
|
|
setupSession: false
|
|
};
|
|
|
|
const {storageVersion, appVersion: savedAppVersion} =
|
|
await browser.storage.local.get(['storageVersion', 'appVersion']);
|
|
|
|
if (!storageVersion) {
|
|
startup.install = true;
|
|
}
|
|
|
|
if (
|
|
storageVersion !== storageRevisions.local ||
|
|
savedAppVersion !== appVersion
|
|
) {
|
|
startup.update = true;
|
|
}
|
|
|
|
if (mv3 && (await isSessionStartup())) {
|
|
startup.session = true;
|
|
}
|
|
|
|
if (startup.install || startup.update) {
|
|
startup.setupInstance = true;
|
|
}
|
|
|
|
if (startup.session || !mv3) {
|
|
startup.setupSession = true;
|
|
}
|
|
|
|
return startup;
|
|
}
|
|
|
|
let startupState;
|
|
async function getStartupState({event = ''} = {}) {
|
|
if (!startupState) {
|
|
startupState = isStartup();
|
|
startupState.events = [];
|
|
}
|
|
|
|
if (event) {
|
|
startupState.events.push(event);
|
|
}
|
|
|
|
const startup = await startupState;
|
|
|
|
if (startupState.events.includes('install')) {
|
|
startup.setupInstance = true;
|
|
}
|
|
if (startupState.events.includes('startup')) {
|
|
startup.setupSession = true;
|
|
}
|
|
|
|
return startup;
|
|
}
|
|
|
|
function sendNativeMessage(port, message, {timeout = 10000} = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const id = uuidv4();
|
|
message.id = id;
|
|
|
|
const messageCallback = function (msg) {
|
|
if (msg.id !== id) {
|
|
return;
|
|
}
|
|
removeListeners();
|
|
resolve(msg);
|
|
};
|
|
const errorCallback = function () {
|
|
removeListeners();
|
|
reject('No response from native app');
|
|
};
|
|
const removeListeners = function () {
|
|
self.clearTimeout(timeoutId);
|
|
port.onMessage.removeListener(messageCallback);
|
|
port.onDisconnect.removeListener(errorCallback);
|
|
};
|
|
|
|
const timeoutId = self.setTimeout(function () {
|
|
errorCallback();
|
|
}, timeout);
|
|
|
|
port.onMessage.addListener(messageCallback);
|
|
port.onDisconnect.addListener(errorCallback);
|
|
|
|
port.postMessage(message);
|
|
});
|
|
}
|
|
|
|
async function pingClientApp({
|
|
start = true,
|
|
stop = true,
|
|
checkResponse = true
|
|
} = {}) {
|
|
if (start) {
|
|
await browser.runtime.sendMessage({id: 'startClientApp'});
|
|
}
|
|
|
|
const rsp = await browser.runtime.sendMessage({
|
|
id: 'messageClientApp',
|
|
message: {command: 'ping'}
|
|
});
|
|
|
|
if (stop) {
|
|
await browser.runtime.sendMessage({id: 'stopClientApp'});
|
|
}
|
|
|
|
if (checkResponse && (!rsp.success || rsp.data !== 'pong')) {
|
|
throw new Error(`Client app response: ${rsp.data}`);
|
|
}
|
|
|
|
return rsp;
|
|
}
|
|
|
|
function meanSleep(ms) {
|
|
const maxDeviation = 0.1 * ms;
|
|
return sleep(getRandomInt(ms - maxDeviation, ms + maxDeviation));
|
|
}
|
|
|
|
export {
|
|
showNotification,
|
|
getListItems,
|
|
insertBaseModule,
|
|
loadFonts,
|
|
processMessageResponse,
|
|
configApp,
|
|
getAppTheme,
|
|
addSystemThemeListener,
|
|
addAppThemeListener,
|
|
addThemeListener,
|
|
getOpenerTabId,
|
|
showPage,
|
|
autoShowContributePage,
|
|
updateUseCount,
|
|
processAppUse,
|
|
showContributePage,
|
|
showOptionsPage,
|
|
setAppVersion,
|
|
isSessionStartup,
|
|
isStartup,
|
|
getStartupState,
|
|
sendNativeMessage,
|
|
pingClientApp,
|
|
meanSleep
|
|
};
|