index.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import browser from 'webextension-polyfill';
  2. import findSelector from '@/lib/findSelector';
  3. import { toCamelCase } from '@/utils/helper';
  4. import { nanoid } from 'nanoid';
  5. import automa from '@business';
  6. import handleSelector from './handleSelector';
  7. import blocksHandler from './blocksHandler';
  8. import showExecutedBlock from './showExecutedBlock';
  9. import shortcutListener from './services/shortcutListener';
  10. import initCommandPalette from './commandPalette';
  11. // import elementObserver from './elementObserver';
  12. import { elementSelectorInstance } from './utils';
  13. const isMainFrame = window.self === window.top;
  14. function messageToFrame(frameElement, blockData) {
  15. return new Promise((resolve, reject) => {
  16. function onMessage({ data }) {
  17. if (data.type !== 'automa:block-execute-result') return;
  18. if (data.result?.$isError) {
  19. const error = new Error(data.result.message);
  20. error.data = data.result.data;
  21. reject(error);
  22. } else {
  23. resolve(data.result);
  24. }
  25. window.removeEventListener('message', onMessage);
  26. }
  27. window.addEventListener('message', onMessage);
  28. frameElement.contentWindow.postMessage(
  29. {
  30. type: 'automa:execute-block',
  31. blockData: { ...blockData, frameSelector: '' },
  32. },
  33. '*'
  34. );
  35. });
  36. }
  37. async function executeBlock(data) {
  38. const removeExecutedBlock = showExecutedBlock(data, data.executedBlockOnWeb);
  39. if (data.data?.selector?.includes('|>') && isMainFrame) {
  40. const [frameSelector, selector] = data.data.selector.split(/\|>(.+)/);
  41. const frameElement = document.querySelector(frameSelector);
  42. const frameError = (message) => {
  43. const error = new Error(message);
  44. error.data = { selector: frameSelector };
  45. return error;
  46. };
  47. if (!frameElement) throw frameError('iframe-not-found');
  48. const isFrameEelement = ['IFRAME', 'FRAME'].includes(frameElement.tagName);
  49. if (!isFrameEelement) throw frameError('not-iframe');
  50. data.data.selector = selector;
  51. data.data.$frameSelector = frameSelector;
  52. if (frameElement.contentDocument) {
  53. data.frameSelector = frameSelector;
  54. } else {
  55. const result = await messageToFrame(frameElement, data);
  56. return result;
  57. }
  58. }
  59. const handlers = blocksHandler();
  60. const handler = handlers[toCamelCase(data.name || data.label)];
  61. if (handler) {
  62. const result = await handler(data, { handleSelector });
  63. removeExecutedBlock();
  64. return result;
  65. }
  66. const error = new Error(`"${data.label}" doesn't have a handler`);
  67. console.error(error);
  68. throw error;
  69. }
  70. function messageListener({ data, source }) {
  71. if (data.type === 'automa:get-frame' && isMainFrame) {
  72. let frameRect = { x: 0, y: 0 };
  73. document.querySelectorAll('iframe').forEach((iframe) => {
  74. if (iframe.contentWindow !== source) return;
  75. frameRect = iframe.getBoundingClientRect();
  76. });
  77. source.postMessage(
  78. {
  79. frameRect,
  80. type: 'automa:the-frame-rect',
  81. },
  82. '*'
  83. );
  84. return;
  85. }
  86. if (data.type === 'automa:execute-block') {
  87. executeBlock(data.blockData)
  88. .then((result) => {
  89. window.top.postMessage(
  90. {
  91. result,
  92. type: 'automa:block-execute-result',
  93. },
  94. '*'
  95. );
  96. })
  97. .catch((error) => {
  98. console.error(error);
  99. window.top.postMessage(
  100. {
  101. result: {
  102. $isError: true,
  103. message: error.message,
  104. data: error.data || {},
  105. },
  106. type: 'automa:block-execute-result',
  107. },
  108. '*'
  109. );
  110. });
  111. }
  112. }
  113. (() => {
  114. if (window.isAutomaInjected) return;
  115. initCommandPalette();
  116. let contextElement = null;
  117. let $ctxTextSelection = '';
  118. window.isAutomaInjected = true;
  119. window.addEventListener('message', messageListener);
  120. window.addEventListener('contextmenu', ({ target }) => {
  121. contextElement = target;
  122. $ctxTextSelection = window.getSelection().toString();
  123. });
  124. if (isMainFrame) {
  125. shortcutListener();
  126. // window.addEventListener('load', elementObserver);
  127. }
  128. automa('content');
  129. browser.runtime.onMessage.addListener((data) => {
  130. return new Promise((resolve, reject) => {
  131. if (data.isBlock) {
  132. executeBlock(data)
  133. .then(resolve)
  134. .catch((error) => {
  135. const elNotFound = error.message === 'element-not-found';
  136. const isLoopItem = data.data?.selector?.includes('automa-loop');
  137. if (elNotFound && isLoopItem) {
  138. const findLoopEl = data.loopEls.find(({ url }) =>
  139. window.location.href.includes(url)
  140. );
  141. const blockData = { ...data.data, ...findLoopEl, multiple: true };
  142. const loopBlock = {
  143. ...data,
  144. onlyGenerate: true,
  145. data: blockData,
  146. };
  147. blocksHandler()
  148. .loopData(loopBlock)
  149. .then(() => {
  150. executeBlock(data).then(resolve).catch(reject);
  151. })
  152. .catch((blockError) => {
  153. reject(blockError);
  154. });
  155. return;
  156. }
  157. reject(error);
  158. });
  159. } else {
  160. switch (data.type) {
  161. case 'content-script-exists':
  162. resolve(true);
  163. break;
  164. case 'automa-element-selector': {
  165. const selectorInstance = elementSelectorInstance();
  166. resolve(selectorInstance);
  167. break;
  168. }
  169. case 'context-element': {
  170. let $ctxLink = '';
  171. let $ctxMediaUrl = '';
  172. let $ctxElSelector = '';
  173. if (contextElement) {
  174. $ctxElSelector = findSelector(contextElement);
  175. const tag = contextElement.tagName;
  176. if (tag === 'A') {
  177. $ctxLink = contextElement.href;
  178. }
  179. const mediaTags = ['AUDIO', 'VIDEO', 'IMG'];
  180. if (mediaTags.includes(tag)) {
  181. let mediaSrc = contextElement.src || '';
  182. if (!mediaSrc.src) {
  183. const sourceEl = contextElement.querySelector('source');
  184. if (sourceEl) mediaSrc = sourceEl.src;
  185. }
  186. $ctxMediaUrl = mediaSrc;
  187. }
  188. contextElement = null;
  189. }
  190. if (!$ctxTextSelection) {
  191. $ctxTextSelection = window.getSelection().toString();
  192. }
  193. resolve({
  194. $ctxElSelector,
  195. $ctxTextSelection,
  196. $ctxLink,
  197. $ctxMediaUrl,
  198. });
  199. break;
  200. }
  201. default:
  202. resolve(null);
  203. }
  204. }
  205. });
  206. });
  207. })();
  208. // Auto install only works on Chrome
  209. (async function autoInstall() {
  210. const link = window.location.href;
  211. if (/.+\.automa\.json$/.test(link)) {
  212. const accept = window.confirm(
  213. 'Do you want to add this workflow into Automa?'
  214. );
  215. if (!accept) return;
  216. const workflow = JSON.parse(document.body.innerText);
  217. const { workflows: workflowsStorage } = await browser.storage.local.get(
  218. 'workflows'
  219. );
  220. const workflowId = nanoid();
  221. const workflowData = {
  222. ...workflow,
  223. id: workflowId,
  224. dataColumns: [],
  225. createdAt: Date.now(),
  226. table: workflow.table || workflow.dataColumns,
  227. };
  228. if (Array.isArray(workflowsStorage)) {
  229. workflowsStorage.push(workflowData);
  230. } else {
  231. workflowsStorage[workflowId] = workflowData;
  232. }
  233. await browser.storage.local.set({ workflows: workflowsStorage });
  234. alert('Workflow installed');
  235. }
  236. })();