index.js 9.3 KB

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