/* eslint-disable no-underscore-dangle */ import browser from 'webextension-polyfill'; import { objectHasKey, fileSaver, isObject } from '@/utils/helper'; import { tasks } from '@/utils/shared'; import dataExporter, { generateJSON } from '@/utils/data-exporter'; import compareBlockValue from '@/utils/compare-block-value'; import errorMessage from './error-message'; import { executeWebhook } from '@/utils/webhookUtil'; function getBlockConnection(block, index = 1) { const blockId = block.outputs[`output_${index}`]?.connections[0]?.node; return blockId; } function convertData(data, type) { let result = data; switch (type) { case 'integer': result = +data.replace(/\D+/g, ''); break; case 'boolean': result = Boolean(data); break; default: } return result; } function generateBlockError(block, code) { const message = errorMessage(code || 'no-tab', tasks[block.name]); const error = new Error(message); error.nextBlockId = getBlockConnection(block); return error; } function executeContentScript(tabId) { return new Promise((resolve, reject) => { let frameTimeout; let timeout; const frames = {}; const onMessageListener = (_, sender) => { if (sender.frameId !== 0) frames[sender.url] = sender.frameId; clearTimeout(frameTimeout); frameTimeout = setTimeout(() => { clearTimeout(timeout); browser.runtime.onMessage.removeListener(onMessageListener); resolve(frames); }, 250); }; browser.tabs .executeScript(tabId, { file: './contentScript.bundle.js', allFrames: true, }) .then(() => { browser.tabs.sendMessage(tabId, { type: 'give-me-the-frame-id', }); browser.runtime.onMessage.addListener(onMessageListener); timeout = setTimeout(() => { clearTimeout(frameTimeout); resolve(frames); }, 5000); }) .catch((error) => { console.error(error); reject(error); }); }); } export async function closeTab(block) { const nextBlockId = getBlockConnection(block); try { const { data } = block; let tabIds; if (data.activeTab && this.tabId) { tabIds = this.tabId; } else if (data.url) { tabIds = (await browser.tabs.query({ url: data.url })).map( (tab) => tab.id ); } if (tabIds) await browser.tabs.remove(tabIds); return { nextBlockId, data: '', }; } catch (error) { const errorInstance = typeof error === 'string' ? new Error(error) : error; errorInstance.nextBlockId = nextBlockId; throw error; } } export async function trigger(block) { const nextBlockId = getBlockConnection(block); try { if (block.data.type === 'visit-web' && this.tabId) { this.frames = executeContentScript(this.tabId); } return { nextBlockId, data: '' }; } catch (error) { const errorInstance = new Error(error); errorInstance.nextBlockId = nextBlockId; throw errorInstance; } } export function loopBreakpoint(block, prevBlockData) { return new Promise((resolve) => { const currentLoop = this.loopList[block.data.loopId]; if ( currentLoop && currentLoop.index < currentLoop.maxLoop - 1 && currentLoop.index <= currentLoop.data.length - 1 ) { resolve({ data: '', nextBlockId: currentLoop.blockId, }); } else { resolve({ data: prevBlockData, nextBlockId: getBlockConnection(block), }); } }); } export function loopData(block) { return new Promise((resolve) => { const { data } = block; if (this.loopList[data.loopId]) { this.loopList[data.loopId].index += 1; this.loopData[data.loopId] = this.loopList[data.loopId].data[this.loopList[data.loopId].index]; } else { const currLoopData = data.loopThrough === 'data-columns' ? generateJSON(Object.keys(this.data), this.data) : JSON.parse(data.loopData); this.loopList[data.loopId] = { index: 0, data: currLoopData, id: data.loopId, blockId: block.id, maxLoop: data.maxLoop || currLoopData.length, }; /* eslint-disable-next-line */ this.loopData[data.loopId] = currLoopData[0]; } resolve({ data: this.loopData[data.loopId], nextBlockId: getBlockConnection(block), }); }); } export function goBack(block) { return new Promise((resolve, reject) => { const nextBlockId = getBlockConnection(block); if (!this.tabId) { reject(generateBlockError(block)); return; } browser.tabs .goBack(this.tabId) .then(() => { resolve({ nextBlockId, data: '', }); }) .catch((error) => { error.nextBlockId = nextBlockId; reject(error); }); }); } export function forwardPage(block) { return new Promise((resolve, reject) => { const nextBlockId = getBlockConnection(block); if (!this.tabId) { reject(generateBlockError(block)); return; } browser.tabs .goForward(this.tabId) .then(() => { resolve({ nextBlockId, data: '', }); }) .catch((error) => { error.nextBlockId = nextBlockId; reject(error); }); }); } function tabUpdatedListener(tab) { return new Promise((resolve, reject) => { this._listener({ name: 'tab-updated', id: tab.id, callback: (tabId, changeInfo, deleteListener) => { if (changeInfo.status !== 'complete') return; deleteListener(); executeContentScript(tabId).then(resolve, reject); }, }); }); } export async function newTab(block) { try { const { updatePrevTab, url, active } = block.data; if (updatePrevTab && this.tabId) { await browser.tabs.update(this.tabId, { url, active }); } else { const { id, windowId } = await browser.tabs.create({ url, active }); this.tabId = id; this.windowId = windowId; } this.frameId = 0; this.frames = await tabUpdatedListener.call(this, { id: this.tabId }); return { data: url, nextBlockId: getBlockConnection(block), }; } catch (error) { console.error(error); throw error; } } export async function activeTab(block) { const nextBlockId = getBlockConnection(block); try { const data = { nextBlockId, data: '', }; if (this.tabId) { await browser.tabs.update(this.tabId, { active: true }); return data; } const [tab] = await browser.tabs.query({ active: true, currentWindow: true, }); this.frames = await executeContentScript(tab.id); this.frameId = 0; this.tabId = tab.id; this.windowId = tab.windowId; return data; } catch (error) { console.error(error); return { data: '', message: error.message || error, nextBlockId, }; } } export async function takeScreenshot(block) { const nextBlockId = getBlockConnection(block); const { ext, quality, captureActiveTab, fileName } = block.data; function saveImage(uri) { const image = new Image(); image.onload = () => { const name = `${fileName || 'Screenshot'}.${ext || 'png'}`; const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const context = canvas.getContext('2d'); context.drawImage(image, 0, 0); fileSaver(name, canvas.toDataURL()); }; image.src = uri; } try { const options = { quality, format: ext || 'png', }; if (captureActiveTab) { if (!this.tabId) { throw new Error(errorMessage('no-tab', block)); } const [tab] = await browser.tabs.query({ active: true, currentWindow: true, }); await browser.windows.update(this.windowId, { focused: true }); await browser.tabs.update(this.tabId, { active: true }); await new Promise((resolve) => setTimeout(resolve, 500)); const uri = await browser.tabs.captureVisibleTab(options); if (tab) { await browser.windows.update(tab.windowId, { focused: true }); await browser.tabs.update(tab.id, { active: true }); } saveImage(uri); } else { const uri = await browser.tabs.captureVisibleTab(options); saveImage(uri); } return { data: '', nextBlockId }; } catch (error) { error.nextBlockId = nextBlockId; throw error; } } export async function switchTo(block) { const nextBlockId = getBlockConnection(block); try { if (block.data.windowType === 'main-window') { this.frameId = 0; return { data: '', nextBlockId, }; } const { url } = await this._sendMessageToTab(block, { frameId: 0 }); if (objectHasKey(this.frames, url)) { this.frameId = this.frames[url]; return { data: this.frameId, nextBlockId, }; } throw new Error(errorMessage('no-iframe-id', block.data)); } catch (error) { error.nextBlockId = nextBlockId; throw error; } } export async function interactionHandler(block) { const nextBlockId = getBlockConnection(block); try { const data = await this._sendMessageToTab(block, { frameId: this.frameId || 0, }); if (block.name === 'link') await new Promise((resolve) => setTimeout(resolve, 5000)); if (data?.isError) { const error = new Error(data.message); error.nextBlockId = nextBlockId; throw error; } const getColumn = (name) => this.workflow.dataColumns.find((item) => item.name === name) || { name: 'column', type: 'text', }; const pushData = (column, value) => { this.data[column.name]?.push(convertData(value, column.type)); }; if (objectHasKey(block.data, 'dataColumn')) { const column = getColumn(block.data.dataColumn); if (block.data.saveData) { if (Array.isArray(data)) { data.forEach((item) => { pushData(column, item); }); } else { pushData(column, data); } } } else if (block.name === 'javascript-code') { const memoColumn = {}; const pushObjectData = (obj) => { Object.entries(obj).forEach(([key, value]) => { let column; if (memoColumn[key]) { column = memoColumn[key]; } else { const currentColumn = getColumn(key); column = currentColumn; memoColumn[key] = currentColumn; } pushData(column, value); }); }; if (Array.isArray(data)) { data.forEach((obj) => { if (isObject(obj)) pushObjectData(obj); }); } else if (isObject(data)) { pushObjectData(data); } } return { data, nextBlockId, }; } catch (error) { error.nextBlockId = nextBlockId; throw error; } } export function delay(block) { return new Promise((resolve) => { setTimeout(() => { resolve({ nextBlockId: getBlockConnection(block), data: '', }); }, block.data.time); }); } export function exportData(block) { return new Promise((resolve) => { dataExporter(this.data, block.data); resolve({ data: '', nextBlockId: getBlockConnection(block), }); }); } export function elementExists(block) { return new Promise((resolve, reject) => { this._sendMessageToTab(block) .then((data) => { resolve({ data, nextBlockId: getBlockConnection(block, data ? 1 : 2), }); }) .catch((error) => { error.nextBlockId = getBlockConnection(block); reject(error); }); }); } export function conditions({ data, outputs }, prevBlockData) { return new Promise((resolve, reject) => { if (data.conditions.length === 0) { reject(new Error('Conditions is empty')); return; } let outputIndex = data.conditions.length + 1; let resultData = ''; const prevData = Array.isArray(prevBlockData) ? prevBlockData[0] : prevBlockData; data.conditions.forEach(({ type, value }, index) => { const result = compareBlockValue(type, prevData, value); if (result) { resultData = value; outputIndex = index + 1; } }); resolve({ data: resultData, nextBlockId: getBlockConnection({ outputs }, outputIndex), }); }); } export function repeatTask({ data, id, outputs }) { return new Promise((resolve) => { if (this.repeatedTasks[id] >= data.repeatFor) { resolve({ data: data.repeatFor, nextBlockId: getBlockConnection({ outputs }), }); } else { this.repeatedTasks[id] = (this.repeatedTasks[id] || 1) + 1; resolve({ data: data.repeatFor, nextBlockId: getBlockConnection({ outputs }, 2), }); } }); } export function webhook({ data, outputs }) { return new Promise((resolve, reject) => { if (!data.url) { reject(new Error('URL is empty')); return; } if (!data.url.startsWith('http')) { reject(new Error('URL is not valid')); return; } executeWebhook(data) .then(() => { resolve({ data: '', nextBlockId: getBlockConnection({ outputs }), }); }) .catch((error) => { reject(error); }); }); }