1
0

blocks-handler.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /* eslint-disable no-underscore-dangle */
  2. import browser from 'webextension-polyfill';
  3. import { objectHasKey, fileSaver, isObject } from '@/utils/helper';
  4. import { tasks } from '@/utils/shared';
  5. import dataExporter, { generateJSON } from '@/utils/data-exporter';
  6. import compareBlockValue from '@/utils/compare-block-value';
  7. import errorMessage from './error-message';
  8. import { executeWebhook } from '@/utils/webhookUtil';
  9. function getBlockConnection(block, index = 1) {
  10. const blockId = block.outputs[`output_${index}`]?.connections[0]?.node;
  11. return blockId;
  12. }
  13. function convertData(data, type) {
  14. let result = data;
  15. switch (type) {
  16. case 'integer':
  17. result = +data.replace(/\D+/g, '');
  18. break;
  19. case 'boolean':
  20. result = Boolean(data);
  21. break;
  22. default:
  23. }
  24. return result;
  25. }
  26. function generateBlockError(block, code) {
  27. const message = errorMessage(code || 'no-tab', tasks[block.name]);
  28. const error = new Error(message);
  29. error.nextBlockId = getBlockConnection(block);
  30. return error;
  31. }
  32. function executeContentScript(tabId) {
  33. return new Promise((resolve, reject) => {
  34. let frameTimeout;
  35. let timeout;
  36. const frames = {};
  37. const onMessageListener = (_, sender) => {
  38. if (sender.frameId !== 0) frames[sender.url] = sender.frameId;
  39. clearTimeout(frameTimeout);
  40. frameTimeout = setTimeout(() => {
  41. clearTimeout(timeout);
  42. browser.runtime.onMessage.removeListener(onMessageListener);
  43. resolve(frames);
  44. }, 250);
  45. };
  46. browser.tabs
  47. .executeScript(tabId, {
  48. file: './contentScript.bundle.js',
  49. allFrames: true,
  50. })
  51. .then(() => {
  52. browser.tabs.sendMessage(tabId, {
  53. type: 'give-me-the-frame-id',
  54. });
  55. browser.runtime.onMessage.addListener(onMessageListener);
  56. timeout = setTimeout(() => {
  57. clearTimeout(frameTimeout);
  58. resolve(frames);
  59. }, 5000);
  60. })
  61. .catch((error) => {
  62. console.error(error);
  63. reject(error);
  64. });
  65. });
  66. }
  67. export async function closeTab(block) {
  68. const nextBlockId = getBlockConnection(block);
  69. try {
  70. const { data } = block;
  71. let tabIds;
  72. if (data.activeTab && this.tabId) {
  73. tabIds = this.tabId;
  74. } else if (data.url) {
  75. tabIds = (await browser.tabs.query({ url: data.url })).map(
  76. (tab) => tab.id
  77. );
  78. }
  79. if (tabIds) await browser.tabs.remove(tabIds);
  80. return {
  81. nextBlockId,
  82. data: '',
  83. };
  84. } catch (error) {
  85. const errorInstance = typeof error === 'string' ? new Error(error) : error;
  86. errorInstance.nextBlockId = nextBlockId;
  87. throw error;
  88. }
  89. }
  90. export async function trigger(block) {
  91. const nextBlockId = getBlockConnection(block);
  92. try {
  93. if (block.data.type === 'visit-web' && this.tabId) {
  94. this.frames = executeContentScript(this.tabId);
  95. }
  96. return { nextBlockId, data: '' };
  97. } catch (error) {
  98. const errorInstance = new Error(error);
  99. errorInstance.nextBlockId = nextBlockId;
  100. throw errorInstance;
  101. }
  102. }
  103. export function loopBreakpoint(block, prevBlockData) {
  104. return new Promise((resolve) => {
  105. const currentLoop = this.loopList[block.data.loopId];
  106. if (
  107. currentLoop &&
  108. currentLoop.index < currentLoop.maxLoop - 1 &&
  109. currentLoop.index <= currentLoop.data.length - 1
  110. ) {
  111. resolve({
  112. data: '',
  113. nextBlockId: currentLoop.blockId,
  114. });
  115. } else {
  116. resolve({
  117. data: prevBlockData,
  118. nextBlockId: getBlockConnection(block),
  119. });
  120. }
  121. });
  122. }
  123. export function loopData(block) {
  124. return new Promise((resolve) => {
  125. const { data } = block;
  126. if (this.loopList[data.loopId]) {
  127. this.loopList[data.loopId].index += 1;
  128. this.loopData[data.loopId] =
  129. this.loopList[data.loopId].data[this.loopList[data.loopId].index];
  130. } else {
  131. const currLoopData =
  132. data.loopThrough === 'data-columns'
  133. ? generateJSON(Object.keys(this.data), this.data)
  134. : JSON.parse(data.loopData);
  135. this.loopList[data.loopId] = {
  136. index: 0,
  137. data: currLoopData,
  138. id: data.loopId,
  139. blockId: block.id,
  140. maxLoop: data.maxLoop || currLoopData.length,
  141. };
  142. /* eslint-disable-next-line */
  143. this.loopData[data.loopId] = currLoopData[0];
  144. }
  145. resolve({
  146. data: this.loopData[data.loopId],
  147. nextBlockId: getBlockConnection(block),
  148. });
  149. });
  150. }
  151. export function goBack(block) {
  152. return new Promise((resolve, reject) => {
  153. const nextBlockId = getBlockConnection(block);
  154. if (!this.tabId) {
  155. reject(generateBlockError(block));
  156. return;
  157. }
  158. browser.tabs
  159. .goBack(this.tabId)
  160. .then(() => {
  161. resolve({
  162. nextBlockId,
  163. data: '',
  164. });
  165. })
  166. .catch((error) => {
  167. error.nextBlockId = nextBlockId;
  168. reject(error);
  169. });
  170. });
  171. }
  172. export function forwardPage(block) {
  173. return new Promise((resolve, reject) => {
  174. const nextBlockId = getBlockConnection(block);
  175. if (!this.tabId) {
  176. reject(generateBlockError(block));
  177. return;
  178. }
  179. browser.tabs
  180. .goForward(this.tabId)
  181. .then(() => {
  182. resolve({
  183. nextBlockId,
  184. data: '',
  185. });
  186. })
  187. .catch((error) => {
  188. error.nextBlockId = nextBlockId;
  189. reject(error);
  190. });
  191. });
  192. }
  193. function tabUpdatedListener(tab) {
  194. return new Promise((resolve, reject) => {
  195. this._listener({
  196. name: 'tab-updated',
  197. id: tab.id,
  198. callback: (tabId, changeInfo, deleteListener) => {
  199. if (changeInfo.status !== 'complete') return;
  200. deleteListener();
  201. executeContentScript(tabId).then(resolve, reject);
  202. },
  203. });
  204. });
  205. }
  206. export async function newTab(block) {
  207. try {
  208. const { updatePrevTab, url, active } = block.data;
  209. if (updatePrevTab && this.tabId) {
  210. await browser.tabs.update(this.tabId, { url, active });
  211. } else {
  212. const { id, windowId } = await browser.tabs.create({ url, active });
  213. this.tabId = id;
  214. this.windowId = windowId;
  215. }
  216. this.frameId = 0;
  217. this.frames = await tabUpdatedListener.call(this, { id: this.tabId });
  218. return {
  219. data: url,
  220. nextBlockId: getBlockConnection(block),
  221. };
  222. } catch (error) {
  223. console.error(error);
  224. throw error;
  225. }
  226. }
  227. export async function activeTab(block) {
  228. const nextBlockId = getBlockConnection(block);
  229. try {
  230. const data = {
  231. nextBlockId,
  232. data: '',
  233. };
  234. if (this.tabId) {
  235. await browser.tabs.update(this.tabId, { active: true });
  236. return data;
  237. }
  238. const [tab] = await browser.tabs.query({
  239. active: true,
  240. currentWindow: true,
  241. });
  242. this.frames = await executeContentScript(tab.id);
  243. this.frameId = 0;
  244. this.tabId = tab.id;
  245. this.windowId = tab.windowId;
  246. return data;
  247. } catch (error) {
  248. console.error(error);
  249. return {
  250. data: '',
  251. message: error.message || error,
  252. nextBlockId,
  253. };
  254. }
  255. }
  256. export async function takeScreenshot(block) {
  257. const nextBlockId = getBlockConnection(block);
  258. const { ext, quality, captureActiveTab, fileName } = block.data;
  259. function saveImage(uri) {
  260. const image = new Image();
  261. image.onload = () => {
  262. const name = `${fileName || 'Screenshot'}.${ext || 'png'}`;
  263. const canvas = document.createElement('canvas');
  264. canvas.width = image.width;
  265. canvas.height = image.height;
  266. const context = canvas.getContext('2d');
  267. context.drawImage(image, 0, 0);
  268. fileSaver(name, canvas.toDataURL());
  269. };
  270. image.src = uri;
  271. }
  272. try {
  273. const options = {
  274. quality,
  275. format: ext || 'png',
  276. };
  277. if (captureActiveTab) {
  278. if (!this.tabId) {
  279. throw new Error(errorMessage('no-tab', block));
  280. }
  281. const [tab] = await browser.tabs.query({
  282. active: true,
  283. currentWindow: true,
  284. });
  285. await browser.windows.update(this.windowId, { focused: true });
  286. await browser.tabs.update(this.tabId, { active: true });
  287. await new Promise((resolve) => setTimeout(resolve, 500));
  288. const uri = await browser.tabs.captureVisibleTab(options);
  289. if (tab) {
  290. await browser.windows.update(tab.windowId, { focused: true });
  291. await browser.tabs.update(tab.id, { active: true });
  292. }
  293. saveImage(uri);
  294. } else {
  295. const uri = await browser.tabs.captureVisibleTab(options);
  296. saveImage(uri);
  297. }
  298. return { data: '', nextBlockId };
  299. } catch (error) {
  300. error.nextBlockId = nextBlockId;
  301. throw error;
  302. }
  303. }
  304. export async function switchTo(block) {
  305. const nextBlockId = getBlockConnection(block);
  306. try {
  307. if (block.data.windowType === 'main-window') {
  308. this.frameId = 0;
  309. return {
  310. data: '',
  311. nextBlockId,
  312. };
  313. }
  314. const { url } = await this._sendMessageToTab(block, { frameId: 0 });
  315. if (objectHasKey(this.frames, url)) {
  316. this.frameId = this.frames[url];
  317. return {
  318. data: this.frameId,
  319. nextBlockId,
  320. };
  321. }
  322. throw new Error(errorMessage('no-iframe-id', block.data));
  323. } catch (error) {
  324. error.nextBlockId = nextBlockId;
  325. throw error;
  326. }
  327. }
  328. export async function interactionHandler(block) {
  329. const nextBlockId = getBlockConnection(block);
  330. try {
  331. const data = await this._sendMessageToTab(block, {
  332. frameId: this.frameId || 0,
  333. });
  334. if (block.name === 'link')
  335. await new Promise((resolve) => setTimeout(resolve, 5000));
  336. if (data?.isError) {
  337. const error = new Error(data.message);
  338. error.nextBlockId = nextBlockId;
  339. throw error;
  340. }
  341. const getColumn = (name) =>
  342. this.workflow.dataColumns.find((item) => item.name === name) || {
  343. name: 'column',
  344. type: 'text',
  345. };
  346. const pushData = (column, value) => {
  347. this.data[column.name]?.push(convertData(value, column.type));
  348. };
  349. if (objectHasKey(block.data, 'dataColumn')) {
  350. const column = getColumn(block.data.dataColumn);
  351. if (block.data.saveData) {
  352. if (Array.isArray(data)) {
  353. data.forEach((item) => {
  354. pushData(column, item);
  355. });
  356. } else {
  357. pushData(column, data);
  358. }
  359. }
  360. } else if (block.name === 'javascript-code') {
  361. const memoColumn = {};
  362. const pushObjectData = (obj) => {
  363. Object.entries(obj).forEach(([key, value]) => {
  364. let column;
  365. if (memoColumn[key]) {
  366. column = memoColumn[key];
  367. } else {
  368. const currentColumn = getColumn(key);
  369. column = currentColumn;
  370. memoColumn[key] = currentColumn;
  371. }
  372. pushData(column, value);
  373. });
  374. };
  375. if (Array.isArray(data)) {
  376. data.forEach((obj) => {
  377. if (isObject(obj)) pushObjectData(obj);
  378. });
  379. } else if (isObject(data)) {
  380. pushObjectData(data);
  381. }
  382. }
  383. return {
  384. data,
  385. nextBlockId,
  386. };
  387. } catch (error) {
  388. error.nextBlockId = nextBlockId;
  389. throw error;
  390. }
  391. }
  392. export function delay(block) {
  393. return new Promise((resolve) => {
  394. setTimeout(() => {
  395. resolve({
  396. nextBlockId: getBlockConnection(block),
  397. data: '',
  398. });
  399. }, block.data.time);
  400. });
  401. }
  402. export function exportData(block) {
  403. return new Promise((resolve) => {
  404. dataExporter(this.data, block.data);
  405. resolve({
  406. data: '',
  407. nextBlockId: getBlockConnection(block),
  408. });
  409. });
  410. }
  411. export function elementExists(block) {
  412. return new Promise((resolve, reject) => {
  413. this._sendMessageToTab(block)
  414. .then((data) => {
  415. resolve({
  416. data,
  417. nextBlockId: getBlockConnection(block, data ? 1 : 2),
  418. });
  419. })
  420. .catch((error) => {
  421. error.nextBlockId = getBlockConnection(block);
  422. reject(error);
  423. });
  424. });
  425. }
  426. export function conditions({ data, outputs }, prevBlockData) {
  427. return new Promise((resolve, reject) => {
  428. if (data.conditions.length === 0) {
  429. reject(new Error('Conditions is empty'));
  430. return;
  431. }
  432. let outputIndex = data.conditions.length + 1;
  433. let resultData = '';
  434. const prevData = Array.isArray(prevBlockData)
  435. ? prevBlockData[0]
  436. : prevBlockData;
  437. data.conditions.forEach(({ type, value }, index) => {
  438. const result = compareBlockValue(type, prevData, value);
  439. if (result) {
  440. resultData = value;
  441. outputIndex = index + 1;
  442. }
  443. });
  444. resolve({
  445. data: resultData,
  446. nextBlockId: getBlockConnection({ outputs }, outputIndex),
  447. });
  448. });
  449. }
  450. export function repeatTask({ data, id, outputs }) {
  451. return new Promise((resolve) => {
  452. if (this.repeatedTasks[id] >= data.repeatFor) {
  453. resolve({
  454. data: data.repeatFor,
  455. nextBlockId: getBlockConnection({ outputs }),
  456. });
  457. } else {
  458. this.repeatedTasks[id] = (this.repeatedTasks[id] || 1) + 1;
  459. resolve({
  460. data: data.repeatFor,
  461. nextBlockId: getBlockConnection({ outputs }, 2),
  462. });
  463. }
  464. });
  465. }
  466. export function webhook({ data, outputs }) {
  467. return new Promise((resolve, reject) => {
  468. if (!data.url) {
  469. reject(new Error('URL is empty'));
  470. return;
  471. }
  472. if (!data.url.startsWith('http')) {
  473. reject(new Error('URL is not valid'));
  474. return;
  475. }
  476. executeWebhook(data)
  477. .then(() => {
  478. resolve({
  479. data: '',
  480. nextBlockId: getBlockConnection({ outputs }),
  481. });
  482. })
  483. .catch((error) => {
  484. reject(error);
  485. });
  486. });
  487. }