index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import browser from 'webextension-polyfill';
  2. import { nanoid } from 'nanoid';
  3. import cloneDeep from 'lodash.clonedeep';
  4. import findSelector from '@/lib/findSelector';
  5. import { sendMessage } from '@/utils/message';
  6. import automa from '@business';
  7. import { toCamelCase, isXPath } from '@/utils/helper';
  8. import handleSelector, {
  9. queryElements,
  10. getDocumentCtx,
  11. } from './handleSelector';
  12. import blocksHandler from './blocksHandler';
  13. import showExecutedBlock from './showExecutedBlock';
  14. import shortcutListener from './services/shortcutListener';
  15. import initCommandPalette from './commandPalette';
  16. // import elementObserver from './elementObserver';
  17. import { elementSelectorInstance } from './utils';
  18. const isMainFrame = window.self === window.top;
  19. function messageToFrame(frameElement, blockData) {
  20. return new Promise((resolve, reject) => {
  21. function onMessage({ data }) {
  22. if (data.type !== 'automa:block-execute-result') return;
  23. if (data.result?.$isError) {
  24. const error = new Error(data.result.message);
  25. error.data = data.result.data;
  26. reject(error);
  27. } else {
  28. resolve(data.result);
  29. }
  30. window.removeEventListener('message', onMessage);
  31. }
  32. window.addEventListener('message', onMessage);
  33. const messageId = `message:${nanoid(4)}`;
  34. browser.storage.local.set({ [messageId]: true }).then(() => {
  35. frameElement.contentWindow.postMessage(
  36. {
  37. messageId,
  38. type: 'automa:execute-block',
  39. blockData: { ...blockData, frameSelector: '' },
  40. },
  41. '*'
  42. );
  43. });
  44. });
  45. }
  46. async function executeBlock(data) {
  47. const removeExecutedBlock = showExecutedBlock(data, data.executedBlockOnWeb);
  48. if (data.data?.selector?.includes('|>')) {
  49. const selectorsArr = data.data.selector.split('|>');
  50. const selector = selectorsArr.pop();
  51. const frameSelector = selectorsArr.join('|>');
  52. const framElSelector = selectorsArr.pop();
  53. let findBy = data?.data?.findBy;
  54. if (!findBy) {
  55. findBy = isXPath(frameSelector) ? 'xpath' : 'cssSelector';
  56. }
  57. const documentCtx = getDocumentCtx(selectorsArr.join('|>'));
  58. const frameElement = await queryElements(
  59. {
  60. findBy,
  61. multiple: false,
  62. waitForSelector: 5000,
  63. selector: framElSelector,
  64. },
  65. documentCtx
  66. );
  67. const frameError = (message) => {
  68. const error = new Error(message);
  69. error.data = { selector: frameSelector };
  70. return error;
  71. };
  72. if (!frameElement) throw frameError('iframe-not-found');
  73. const isFrameEelement = ['IFRAME', 'FRAME'].includes(frameElement.tagName);
  74. if (!isFrameEelement) throw frameError('not-iframe');
  75. const { x, y } = frameElement.getBoundingClientRect();
  76. const iframeDetails = { x, y };
  77. if (isMainFrame) {
  78. iframeDetails.windowWidth = window.innerWidth;
  79. iframeDetails.windowHeight = window.innerHeight;
  80. }
  81. data.data.selector = selector;
  82. data.data.$frameRect = iframeDetails;
  83. data.data.$frameSelector = frameSelector;
  84. if (frameElement.contentDocument) {
  85. data.frameSelector = frameSelector;
  86. } else {
  87. const result = await messageToFrame(frameElement, data);
  88. return result;
  89. }
  90. }
  91. const handlers = blocksHandler();
  92. const handler = handlers[toCamelCase(data.name || data.label)];
  93. if (handler) {
  94. const result = await handler(data, { handleSelector });
  95. removeExecutedBlock();
  96. return result;
  97. }
  98. const error = new Error(`"${data.label}" doesn't have a handler`);
  99. console.error(error);
  100. throw error;
  101. }
  102. async function messageListener({ data, source }) {
  103. try {
  104. if (data.type === 'automa:get-frame' && isMainFrame) {
  105. let frameRect = { x: 0, y: 0 };
  106. document.querySelectorAll('iframe').forEach((iframe) => {
  107. if (iframe.contentWindow !== source) return;
  108. frameRect = iframe.getBoundingClientRect();
  109. });
  110. source.postMessage(
  111. {
  112. frameRect,
  113. type: 'automa:the-frame-rect',
  114. },
  115. '*'
  116. );
  117. return;
  118. }
  119. if (data.type === 'automa:execute-block') {
  120. const messageToken = await browser.storage.local.get(data.messageId);
  121. if (!data.messageId || !messageToken[data.messageId]) {
  122. window.top.postMessage(
  123. {
  124. result: {
  125. $isError: true,
  126. message: 'Block id is empty',
  127. data: {},
  128. },
  129. type: 'automa:block-execute-result',
  130. },
  131. '*'
  132. );
  133. return;
  134. }
  135. await browser.storage.local.remove(data.messageId);
  136. executeBlock(data.blockData)
  137. .then((result) => {
  138. window.top.postMessage(
  139. {
  140. result,
  141. type: 'automa:block-execute-result',
  142. },
  143. '*'
  144. );
  145. })
  146. .catch((error) => {
  147. console.error(error);
  148. window.top.postMessage(
  149. {
  150. result: {
  151. $isError: true,
  152. message: error.message,
  153. data: error.data || {},
  154. },
  155. type: 'automa:block-execute-result',
  156. },
  157. '*'
  158. );
  159. });
  160. }
  161. } catch (error) {
  162. console.error(error);
  163. }
  164. }
  165. (() => {
  166. if (window.isAutomaInjected) return;
  167. initCommandPalette();
  168. let contextElement = null;
  169. let $ctxLink = '';
  170. let $ctxMediaUrl = '';
  171. let $ctxTextSelection = '';
  172. window.isAutomaInjected = true;
  173. window.addEventListener('message', messageListener);
  174. window.addEventListener(
  175. 'contextmenu',
  176. ({ target }) => {
  177. contextElement = target;
  178. $ctxTextSelection = window.getSelection().toString();
  179. const tag = target.tagName;
  180. if (tag === 'A') {
  181. $ctxLink = target.href;
  182. } else {
  183. const closestUrl = target.closest('a');
  184. if (closestUrl) $ctxLink = closestUrl.href;
  185. }
  186. const getMediaSrc = (element) => {
  187. let mediaSrc = element.src || '';
  188. if (!mediaSrc.src) {
  189. const sourceEl = element.querySelector('source');
  190. if (sourceEl) mediaSrc = sourceEl.src;
  191. }
  192. return mediaSrc;
  193. };
  194. const mediaTags = ['AUDIO', 'VIDEO', 'IMG'];
  195. if (mediaTags.includes(tag)) {
  196. $ctxMediaUrl = getMediaSrc(target);
  197. } else {
  198. const closestMedia = target.closest('audio,video,img');
  199. if (closestMedia) $ctxMediaUrl = getMediaSrc(closestMedia);
  200. }
  201. },
  202. true
  203. );
  204. window.isAutomaInjected = true;
  205. window.addEventListener('message', messageListener);
  206. window.addEventListener('contextmenu', ({ target }) => {
  207. contextElement = target;
  208. $ctxTextSelection = window.getSelection().toString();
  209. });
  210. if (isMainFrame) {
  211. shortcutListener();
  212. // window.addEventListener('load', elementObserver);
  213. }
  214. automa('content');
  215. browser.runtime.onMessage.addListener(async (data) => {
  216. const asyncExecuteBlock = async (block) => {
  217. try {
  218. const res = await executeBlock(block);
  219. return res;
  220. } catch (error) {
  221. console.error(error);
  222. const elNotFound = error.message === 'element-not-found';
  223. const isLoopItem = data.data?.selector?.includes('automa-loop');
  224. if (!elNotFound || !isLoopItem) return Promise.reject(error);
  225. const findLoopEl = data.loopEls.find(({ url }) =>
  226. window.location.href.includes(url)
  227. );
  228. const blockData = { ...data.data, ...findLoopEl, multiple: true };
  229. const loopBlock = {
  230. ...data,
  231. onlyGenerate: true,
  232. data: blockData,
  233. };
  234. await blocksHandler().loopData(loopBlock);
  235. return executeBlock(block);
  236. }
  237. };
  238. if (data.isBlock) {
  239. const res = await asyncExecuteBlock(data);
  240. return res;
  241. }
  242. switch (data.type) {
  243. case 'input-workflow-params':
  244. window.initPaletteParams?.(data.data);
  245. return Boolean(window.initPaletteParams);
  246. case 'content-script-exists':
  247. return true;
  248. case 'automa-element-selector': {
  249. return elementSelectorInstance();
  250. }
  251. case 'context-element': {
  252. let $ctxElSelector = '';
  253. if (contextElement) {
  254. $ctxElSelector = findSelector(contextElement);
  255. contextElement = null;
  256. }
  257. if (!$ctxTextSelection) {
  258. $ctxTextSelection = window.getSelection().toString();
  259. }
  260. const cloneContextData = cloneDeep({
  261. $ctxLink,
  262. $ctxMediaUrl,
  263. $ctxElSelector,
  264. $ctxTextSelection,
  265. });
  266. $ctxLink = '';
  267. $ctxMediaUrl = '';
  268. $ctxElSelector = '';
  269. $ctxTextSelection = '';
  270. return cloneContextData;
  271. }
  272. default:
  273. return null;
  274. }
  275. });
  276. })();
  277. window.addEventListener('__automa-fetch__', (event) => {
  278. const { id, resource, type } = event.detail;
  279. const sendResponse = (payload) => {
  280. window.dispatchEvent(
  281. new CustomEvent(`__automa-fetch-response-${id}__`, {
  282. detail: { id, ...payload },
  283. })
  284. );
  285. };
  286. sendMessage('fetch', { type, resource }, 'background')
  287. .then((result) => {
  288. sendResponse({ isError: false, result });
  289. })
  290. .catch((error) => {
  291. sendResponse({ isError: true, result: error.message });
  292. });
  293. });
  294. window.addEventListener('DOMContentLoaded', async () => {
  295. const link = window.location.pathname;
  296. const isAutomaWorkflow = /.+\.automa\.json$/.test(link);
  297. if (!isAutomaWorkflow) return;
  298. const accept = window.confirm(
  299. 'Do you want to add this workflow into Automa?'
  300. );
  301. if (!accept) return;
  302. const workflow = JSON.parse(document.documentElement.innerText);
  303. const { workflows: workflowsStorage } = await browser.storage.local.get(
  304. 'workflows'
  305. );
  306. const workflowId = nanoid();
  307. const workflowData = {
  308. ...workflow,
  309. id: workflowId,
  310. dataColumns: [],
  311. createdAt: Date.now(),
  312. table: workflow.table || workflow.dataColumns,
  313. };
  314. if (Array.isArray(workflowsStorage)) {
  315. workflowsStorage.push(workflowData);
  316. } else {
  317. workflowsStorage[workflowId] = workflowData;
  318. }
  319. await browser.storage.local.set({ workflows: workflowsStorage });
  320. alert('Workflow installed');
  321. });