|
@@ -1,7 +1,8 @@
|
|
|
import browser from 'webextension-polyfill';
|
|
|
import { MessageListener } from '@/utils/message';
|
|
|
import { registerSpecificDay } from '../utils/workflow-trigger';
|
|
|
-import { parseJSON } from '@/utils/helper';
|
|
|
+import { parseJSON, findTriggerBlock } from '@/utils/helper';
|
|
|
+import getFile from '@/utils/get-file';
|
|
|
import WorkflowState from './workflow-state';
|
|
|
import CollectionEngine from './collection-engine';
|
|
|
import WorkflowEngine from './workflow-engine/engine';
|
|
@@ -9,6 +10,7 @@ import blocksHandler from './workflow-engine/blocks-handler';
|
|
|
import WorkflowLogger from './workflow-logger';
|
|
|
import decryptFlow, { getWorkflowPass } from '@/utils/decrypt-flow';
|
|
|
|
|
|
+const validateUrl = (str) => str?.startsWith('http');
|
|
|
const storage = {
|
|
|
async get(key) {
|
|
|
try {
|
|
@@ -32,12 +34,25 @@ const workflow = {
|
|
|
states: new WorkflowState({ storage }),
|
|
|
logger: new WorkflowLogger({ storage }),
|
|
|
async get(workflowId) {
|
|
|
- const { workflows } = await browser.storage.local.get('workflows');
|
|
|
- const findWorkflow = workflows.find(({ id }) => id === workflowId);
|
|
|
+ const { workflows, workflowHosts } = await browser.storage.local.get([
|
|
|
+ 'workflows',
|
|
|
+ 'workflowHosts',
|
|
|
+ ]);
|
|
|
+ let findWorkflow = workflows.find(({ id }) => id === workflowId);
|
|
|
+
|
|
|
+ if (!findWorkflow) {
|
|
|
+ findWorkflow = Object.values(workflowHosts || {}).find(
|
|
|
+ ({ hostId }) => hostId === workflowId
|
|
|
+ );
|
|
|
+
|
|
|
+ if (findWorkflow) findWorkflow.id = findWorkflow.hostId;
|
|
|
+ }
|
|
|
|
|
|
return findWorkflow;
|
|
|
},
|
|
|
execute(workflowData, options) {
|
|
|
+ if (workflowData.isDisabled) return null;
|
|
|
+
|
|
|
if (workflowData.isProtected) {
|
|
|
const flow = parseJSON(workflowData.drawflow, null);
|
|
|
|
|
@@ -65,6 +80,44 @@ const workflow = {
|
|
|
},
|
|
|
};
|
|
|
|
|
|
+async function updateRecording(callback) {
|
|
|
+ const { isRecording, recording } = await browser.storage.local.get([
|
|
|
+ 'isRecording',
|
|
|
+ 'recording',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (!isRecording || !recording) return;
|
|
|
+
|
|
|
+ callback(recording);
|
|
|
+
|
|
|
+ await browser.storage.local.set({ recording });
|
|
|
+}
|
|
|
+async function openDashboard(url) {
|
|
|
+ const tabOptions = {
|
|
|
+ active: true,
|
|
|
+ url: browser.runtime.getURL(
|
|
|
+ `/newtab.html#${typeof url === 'string' ? url : ''}`
|
|
|
+ ),
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const [tab] = await browser.tabs.query({
|
|
|
+ url: browser.runtime.getURL('/newtab.html'),
|
|
|
+ });
|
|
|
+
|
|
|
+ if (tab) {
|
|
|
+ await browser.tabs.update(tab.id, tabOptions);
|
|
|
+
|
|
|
+ if (tab.url.includes('workflows/')) {
|
|
|
+ await browser.tabs.reload(tab.id);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ browser.tabs.create(tabOptions);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ }
|
|
|
+}
|
|
|
async function checkWorkflowStates() {
|
|
|
const states = await workflow.states.get();
|
|
|
// const sessionStates = parseJSON(sessionStorage.getItem('workflowState'), {});
|
|
@@ -89,7 +142,9 @@ async function checkWorkflowStates() {
|
|
|
await storage.set('workflowState', states);
|
|
|
}
|
|
|
checkWorkflowStates();
|
|
|
-async function checkVisitWebTriggers(states, tab) {
|
|
|
+async function checkVisitWebTriggers(changeInfo, tab) {
|
|
|
+ if (!changeInfo.status || changeInfo.status !== 'complete') return;
|
|
|
+
|
|
|
const visitWebTriggers = await storage.get('visitWebTriggers');
|
|
|
const triggeredWorkflow = visitWebTriggers.find(({ url, isRegex }) => {
|
|
|
if (url.trim() === '') return false;
|
|
@@ -103,25 +158,143 @@ async function checkVisitWebTriggers(states, tab) {
|
|
|
if (workflowData) workflow.execute(workflowData);
|
|
|
}
|
|
|
}
|
|
|
+async function checkRecordingWorkflow({ status }, { url, id }) {
|
|
|
+ if (status === 'complete' && validateUrl(url)) {
|
|
|
+ const { isRecording } = await browser.storage.local.get('isRecording');
|
|
|
+
|
|
|
+ if (!isRecording) return;
|
|
|
+
|
|
|
+ await browser.tabs.executeScript(id, {
|
|
|
+ file: 'recordWorkflow.bundle.js',
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
|
|
- if (changeInfo.status === 'complete') {
|
|
|
- await checkVisitWebTriggers(null, tab);
|
|
|
+ checkRecordingWorkflow(changeInfo, tab);
|
|
|
+ checkVisitWebTriggers(changeInfo, tab);
|
|
|
+});
|
|
|
+browser.commands.onCommand.addListener((name) => {
|
|
|
+ if (name === 'open-dashboard') openDashboard();
|
|
|
+});
|
|
|
+browser.webNavigation.onCommitted.addListener(
|
|
|
+ ({ frameId, tabId, url, transitionType }) => {
|
|
|
+ const allowedType = ['link', 'typed', 'form_submit'];
|
|
|
+
|
|
|
+ if (frameId !== 0 || !allowedType.includes(transitionType)) return;
|
|
|
+
|
|
|
+ updateRecording((recording) => {
|
|
|
+ if (tabId !== recording.activeTab.id) return;
|
|
|
+
|
|
|
+ const lastFlow = recording.flows[recording.flows.length - 1];
|
|
|
+ const isClickSubmit =
|
|
|
+ lastFlow.id === 'event-click' && transitionType === 'form_submit';
|
|
|
+
|
|
|
+ if (isClickSubmit) return;
|
|
|
+
|
|
|
+ const isInvalidNewtabFlow =
|
|
|
+ lastFlow &&
|
|
|
+ lastFlow.id === 'new-tab' &&
|
|
|
+ !validateUrl(lastFlow.data.url);
|
|
|
+
|
|
|
+ if (isInvalidNewtabFlow) {
|
|
|
+ lastFlow.data.url = url;
|
|
|
+ lastFlow.description = url;
|
|
|
+ } else if (validateUrl(url)) {
|
|
|
+ if (lastFlow?.id !== 'link' || !lastFlow.isClickLink) {
|
|
|
+ recording.flows.push({
|
|
|
+ id: 'new-tab',
|
|
|
+ description: url,
|
|
|
+ data: {
|
|
|
+ url,
|
|
|
+ updatePrevTab: recording.activeTab.id === tabId,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ recording.activeTab.id = tabId;
|
|
|
+ recording.activeTab.url = url;
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
+);
|
|
|
+browser.tabs.onActivated.addListener(async ({ tabId }) => {
|
|
|
+ const { url, id, title } = await browser.tabs.get(tabId);
|
|
|
+
|
|
|
+ if (!validateUrl(url)) return;
|
|
|
+
|
|
|
+ updateRecording((recording) => {
|
|
|
+ recording.activeTab = { id, url };
|
|
|
+ recording.flows.push({
|
|
|
+ id: 'switch-tab',
|
|
|
+ data: {
|
|
|
+ url,
|
|
|
+ matchPattern: url,
|
|
|
+ createIfNoMatch: true,
|
|
|
+ description: title || url,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|
|
|
-browser.alarms.onAlarm.addListener(({ name }) => {
|
|
|
- workflow.get(name).then((currentWorkflow) => {
|
|
|
- if (!currentWorkflow) return;
|
|
|
+browser.tabs.onCreated.addListener(async (tab) => {
|
|
|
+ const { isRecording, recording } = await browser.storage.local.get([
|
|
|
+ 'isRecording',
|
|
|
+ 'recording',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (!isRecording || !recording) return;
|
|
|
+
|
|
|
+ const url = tab.url || tab.pendingUrl;
|
|
|
+ const lastFlow = recording.flows[recording.flows.length - 1];
|
|
|
+ const invalidPrevFlow =
|
|
|
+ lastFlow && lastFlow.id === 'new-tab' && !validateUrl(lastFlow.data.url);
|
|
|
+
|
|
|
+ if (!invalidPrevFlow) {
|
|
|
+ const validUrl = validateUrl(url) ? url : '';
|
|
|
+
|
|
|
+ recording.flows.push({
|
|
|
+ id: 'new-tab',
|
|
|
+ data: {
|
|
|
+ url: validUrl,
|
|
|
+ description: tab.title || validUrl,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- workflow.execute(currentWorkflow);
|
|
|
+ recording.activeTab = {
|
|
|
+ url,
|
|
|
+ id: tab.id,
|
|
|
+ };
|
|
|
|
|
|
- const triggerBlock = Object.values(
|
|
|
- JSON.parse(currentWorkflow.drawflow).drawflow.Home.data
|
|
|
- ).find((block) => block.name === 'trigger');
|
|
|
+ await browser.storage.local.set({ recording });
|
|
|
+});
|
|
|
+browser.alarms.onAlarm.addListener(async ({ name }) => {
|
|
|
+ const currentWorkflow = await workflow.get(name);
|
|
|
+ if (!currentWorkflow) return;
|
|
|
+
|
|
|
+ const { data } = findTriggerBlock(JSON.parse(currentWorkflow.drawflow)) || {};
|
|
|
+ if (data && data.type === 'interval' && data.fixedDelay) {
|
|
|
+ const workflowState = await workflow.states.get(
|
|
|
+ ({ workflowId }) => name === workflowId
|
|
|
+ );
|
|
|
+
|
|
|
+ if (workflowState) {
|
|
|
+ let { workflowQueue } = await browser.storage.local.get('workflowQueue');
|
|
|
+ workflowQueue = workflowQueue || [];
|
|
|
+
|
|
|
+ if (!workflowQueue.includes(name)) {
|
|
|
+ (workflowQueue = workflowQueue || []).push(name);
|
|
|
+ await browser.storage.local.set({ workflowQueue });
|
|
|
+ }
|
|
|
|
|
|
- if (triggerBlock?.data.type === 'specific-day') {
|
|
|
- registerSpecificDay(currentWorkflow.id, triggerBlock.data);
|
|
|
+ return;
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
+
|
|
|
+ workflow.execute(currentWorkflow);
|
|
|
+
|
|
|
+ if (data && data.type === 'specific-day') {
|
|
|
+ registerSpecificDay(currentWorkflow.id, triggerBlock.data);
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
chrome.runtime.onInstalled.addListener((details) => {
|
|
@@ -172,27 +345,9 @@ message.on('fetch:text', (url) => {
|
|
|
return fetch(url).then((response) => response.text());
|
|
|
});
|
|
|
message.on('open:dashboard', async (url) => {
|
|
|
- const tabOptions = {
|
|
|
- active: true,
|
|
|
- url: browser.runtime.getURL(
|
|
|
- `/newtab.html#${typeof url === 'string' ? url : ''}`
|
|
|
- ),
|
|
|
- };
|
|
|
+ await openDashboard(url);
|
|
|
|
|
|
- try {
|
|
|
- const [tab] = await browser.tabs.query({
|
|
|
- url: browser.runtime.getURL('/newtab.html'),
|
|
|
- });
|
|
|
-
|
|
|
- if (tab) {
|
|
|
- await browser.tabs.update(tab.id, tabOptions);
|
|
|
- await browser.tabs.reload(tab.id);
|
|
|
- } else {
|
|
|
- browser.tabs.create(tabOptions);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error(error);
|
|
|
- }
|
|
|
+ return Promise.resolve(true);
|
|
|
});
|
|
|
message.on('set:active-tab', (tabId) => {
|
|
|
return browser.tabs.update(tabId, { active: true });
|
|
@@ -204,39 +359,7 @@ message.on('get:sender', (_, sender) => {
|
|
|
message.on('get:tab-screenshot', (options) => {
|
|
|
return browser.tabs.captureVisibleTab(options);
|
|
|
});
|
|
|
-message.on('get:file', (path) => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const isFile = /\.(.*)/.test(path);
|
|
|
-
|
|
|
- if (!isFile) {
|
|
|
- reject(new Error(`"${path}" is invalid file path.`));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const fileUrl = path.startsWith('file://') ? path : `file://${path}`;
|
|
|
-
|
|
|
- const xhr = new XMLHttpRequest();
|
|
|
- xhr.responseType = 'blob';
|
|
|
- xhr.onreadystatechange = () => {
|
|
|
- if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
|
- if (xhr.status === 0 || xhr.status === 200) {
|
|
|
- const objUrl = URL.createObjectURL(xhr.response);
|
|
|
-
|
|
|
- resolve({ path, objUrl, type: xhr.response.type });
|
|
|
- } else {
|
|
|
- reject(new Error(xhr.statusText));
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
- xhr.onerror = function () {
|
|
|
- reject(
|
|
|
- new Error(xhr.statusText || `Can't find a file with "${path}" path`)
|
|
|
- );
|
|
|
- };
|
|
|
- xhr.open('GET', fileUrl);
|
|
|
- xhr.send();
|
|
|
- });
|
|
|
-});
|
|
|
+message.on('get:file', (path) => getFile(path));
|
|
|
|
|
|
message.on('collection:execute', (collection) => {
|
|
|
const engine = new CollectionEngine(collection, {
|