blocks-handler.js 12 KB

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