index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. import browser from 'webextension-polyfill';
  2. import dayjs from '@/lib/dayjs';
  3. import { MessageListener } from '@/utils/message';
  4. import { parseJSON, findTriggerBlock } from '@/utils/helper';
  5. import getFile from '@/utils/getFile';
  6. import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
  7. import {
  8. registerSpecificDay,
  9. registerContextMenu,
  10. registerWorkflowTrigger,
  11. } from '../utils/workflowTrigger';
  12. import WorkflowState from './WorkflowState';
  13. import CollectionEngine from './collectionEngine';
  14. import WorkflowEngine from './workflowEngine/engine';
  15. import blocksHandler from './workflowEngine/blocksHandler';
  16. import WorkflowLogger from './WorkflowLogger';
  17. const validateUrl = (str) => str?.startsWith('http');
  18. const browserStorage = {
  19. async get(key) {
  20. try {
  21. const result = await browser.storage.local.get(key);
  22. return result[key];
  23. } catch (error) {
  24. console.error(error);
  25. return [];
  26. }
  27. },
  28. async set(key, value) {
  29. await browser.storage.local.set({ [key]: value });
  30. if (key === 'workflowState') {
  31. sessionStorage.setItem(key, JSON.stringify(value));
  32. }
  33. },
  34. };
  35. const localStateStorage = {
  36. get(key) {
  37. const data = parseJSON(localStorage.getItem(key), null);
  38. return data;
  39. },
  40. set(key, value) {
  41. const data = typeof value === 'object' ? JSON.stringify(value) : value;
  42. return localStorage.setItem(key, data);
  43. },
  44. };
  45. const workflow = {
  46. states: new WorkflowState({ storage: localStateStorage }),
  47. logger: new WorkflowLogger({ storage: browserStorage }),
  48. async get(workflowId) {
  49. const { workflows, workflowHosts } = await browser.storage.local.get([
  50. 'workflows',
  51. 'workflowHosts',
  52. ]);
  53. let findWorkflow = workflows.find(({ id }) => id === workflowId);
  54. if (!findWorkflow) {
  55. findWorkflow = Object.values(workflowHosts || {}).find(
  56. ({ hostId }) => hostId === workflowId
  57. );
  58. if (findWorkflow) findWorkflow.id = findWorkflow.hostId;
  59. }
  60. return findWorkflow;
  61. },
  62. execute(workflowData, options) {
  63. if (workflowData.isDisabled) return null;
  64. if (workflowData.isProtected) {
  65. const flow = parseJSON(workflowData.drawflow, null);
  66. if (!flow) {
  67. const pass = getWorkflowPass(workflowData.pass);
  68. workflowData.drawflow = decryptFlow(workflowData, pass);
  69. }
  70. }
  71. const engine = new WorkflowEngine(workflowData, {
  72. options,
  73. blocksHandler,
  74. logger: this.logger,
  75. states: this.states,
  76. });
  77. if (options?.resume) {
  78. engine.resume(options.state);
  79. } else {
  80. engine.init();
  81. }
  82. return engine;
  83. },
  84. };
  85. async function updateRecording(callback) {
  86. const { isRecording, recording } = await browser.storage.local.get([
  87. 'isRecording',
  88. 'recording',
  89. ]);
  90. if (!isRecording || !recording) return;
  91. callback(recording);
  92. await browser.storage.local.set({ recording });
  93. }
  94. async function openDashboard(url) {
  95. const tabOptions = {
  96. active: true,
  97. url: browser.runtime.getURL(
  98. `/newtab.html#${typeof url === 'string' ? url : ''}`
  99. ),
  100. };
  101. try {
  102. const [tab] = await browser.tabs.query({
  103. url: browser.runtime.getURL('/newtab.html'),
  104. });
  105. if (tab) {
  106. await browser.tabs.update(tab.id, tabOptions);
  107. if (tab.url.includes('workflows/')) {
  108. await browser.tabs.reload(tab.id);
  109. }
  110. } else {
  111. browser.tabs.create(tabOptions);
  112. }
  113. } catch (error) {
  114. console.error(error);
  115. }
  116. }
  117. async function checkWorkflowStates() {
  118. const states = await workflow.states.get();
  119. // const sessionStates = parseJSON(sessionStorage.getItem('workflowState'), {});
  120. states.forEach((state) => {
  121. /* Enable when using manifest 3 */
  122. // const resumeWorkflow =
  123. // !state.isDestroyed && objectHasKey(sessionStates, state.id);
  124. if (false) {
  125. workflow.get(state.workflowId).then((workflowData) => {
  126. workflow.execute(workflowData, {
  127. state,
  128. resume: true,
  129. });
  130. });
  131. } else {
  132. workflow.states.states.delete(state.id);
  133. }
  134. });
  135. await browserStorage.set('workflowState', states);
  136. }
  137. checkWorkflowStates();
  138. async function checkVisitWebTriggers(tabId, tabUrl) {
  139. const workflowState = await workflow.states.get(({ state }) =>
  140. state.tabIds.includes(tabId)
  141. );
  142. const visitWebTriggers = await browserStorage.get('visitWebTriggers');
  143. const triggeredWorkflow = visitWebTriggers?.find(({ url, isRegex, id }) => {
  144. if (url.trim() === '') return false;
  145. const matchUrl = tabUrl.match(isRegex ? new RegExp(url, 'g') : url);
  146. return matchUrl && id !== workflowState?.workflowId;
  147. });
  148. if (triggeredWorkflow) {
  149. const workflowData = await workflow.get(triggeredWorkflow.id);
  150. if (workflowData) workflow.execute(workflowData, { tabId });
  151. }
  152. }
  153. async function checkRecordingWorkflow(tabId, tabUrl) {
  154. if (!validateUrl(tabUrl)) return;
  155. const isRecording = await browserStorage.get('isRecording');
  156. if (!isRecording) return;
  157. await browser.tabs.executeScript(tabId, {
  158. allFrames: true,
  159. file: 'recordWorkflow.bundle.js',
  160. });
  161. }
  162. browser.webNavigation.onCompleted.addListener(
  163. async ({ tabId, url, frameId }) => {
  164. if (frameId > 0) return;
  165. checkRecordingWorkflow(tabId, url);
  166. checkVisitWebTriggers(tabId, url);
  167. }
  168. );
  169. browser.commands.onCommand.addListener((name) => {
  170. if (name === 'open-dashboard') openDashboard();
  171. });
  172. browser.webNavigation.onCommitted.addListener(
  173. ({ frameId, tabId, url, transitionType }) => {
  174. const allowedType = ['link', 'typed'];
  175. if (frameId !== 0 || !allowedType.includes(transitionType)) return;
  176. updateRecording((recording) => {
  177. if (tabId !== recording.activeTab.id) return;
  178. const lastFlow = recording.flows.at(-1) ?? {};
  179. const isInvalidNewtabFlow =
  180. lastFlow &&
  181. lastFlow.id === 'new-tab' &&
  182. !validateUrl(lastFlow.data.url);
  183. if (isInvalidNewtabFlow) {
  184. lastFlow.data.url = url;
  185. lastFlow.description = url;
  186. } else if (validateUrl(url)) {
  187. if (lastFlow?.id !== 'link' || !lastFlow.isClickLink) {
  188. recording.flows.push({
  189. id: 'new-tab',
  190. description: url,
  191. data: {
  192. url,
  193. updatePrevTab: recording.activeTab.id === tabId,
  194. },
  195. });
  196. }
  197. recording.activeTab.id = tabId;
  198. recording.activeTab.url = url;
  199. }
  200. });
  201. }
  202. );
  203. browser.tabs.onActivated.addListener(async ({ tabId }) => {
  204. const { url, id, title } = await browser.tabs.get(tabId);
  205. if (!validateUrl(url)) return;
  206. updateRecording((recording) => {
  207. recording.activeTab = { id, url };
  208. recording.flows.push({
  209. id: 'switch-tab',
  210. description: title,
  211. data: {
  212. url,
  213. matchPattern: url,
  214. createIfNoMatch: true,
  215. },
  216. });
  217. });
  218. });
  219. browser.tabs.onCreated.addListener(async (tab) => {
  220. const { isRecording, recording } = await browser.storage.local.get([
  221. 'isRecording',
  222. 'recording',
  223. ]);
  224. if (!isRecording || !recording) return;
  225. const url = tab.url || tab.pendingUrl;
  226. const lastFlow = recording.flows[recording.flows.length - 1];
  227. const invalidPrevFlow =
  228. lastFlow && lastFlow.id === 'new-tab' && !validateUrl(lastFlow.data.url);
  229. if (!invalidPrevFlow) {
  230. const validUrl = validateUrl(url) ? url : '';
  231. recording.flows.push({
  232. id: 'new-tab',
  233. data: {
  234. url: validUrl,
  235. description: tab.title || validUrl,
  236. },
  237. });
  238. }
  239. recording.activeTab = {
  240. url,
  241. id: tab.id,
  242. };
  243. await browser.storage.local.set({ recording });
  244. });
  245. browser.alarms.onAlarm.addListener(async ({ name }) => {
  246. const currentWorkflow = await workflow.get(name);
  247. if (!currentWorkflow) return;
  248. const { data } = findTriggerBlock(JSON.parse(currentWorkflow.drawflow)) || {};
  249. if (data && data.type === 'interval' && data.fixedDelay) {
  250. const workflowState = await workflow.states.get(
  251. ({ workflowId }) => name === workflowId
  252. );
  253. if (workflowState) {
  254. let { workflowQueue } = await browser.storage.local.get('workflowQueue');
  255. workflowQueue = workflowQueue || [];
  256. if (!workflowQueue.includes(name)) {
  257. (workflowQueue = workflowQueue || []).push(name);
  258. await browser.storage.local.set({ workflowQueue });
  259. }
  260. return;
  261. }
  262. }
  263. workflow.execute(currentWorkflow);
  264. if (data && data.type === 'specific-day') {
  265. registerSpecificDay(currentWorkflow.id, data);
  266. }
  267. });
  268. if (browser.contextMenus && browser.contextMenus.onClicked) {
  269. browser.contextMenus.onClicked.addListener(
  270. async ({ parentMenuItemId, menuItemId }, tab) => {
  271. try {
  272. if (parentMenuItemId !== 'automaContextMenu') return;
  273. const message = await browser.tabs.sendMessage(tab.id, {
  274. frameId: 0,
  275. type: 'context-element',
  276. });
  277. const workflowData = await workflow.get(menuItemId);
  278. workflow.execute(workflowData, {
  279. data: {
  280. variables: message,
  281. },
  282. });
  283. } catch (error) {
  284. console.error(error);
  285. }
  286. }
  287. );
  288. }
  289. browser.runtime.onInstalled.addListener(async ({ reason }) => {
  290. try {
  291. if (reason === 'install') {
  292. await browser.storage.local.set({
  293. logs: [],
  294. shortcuts: {},
  295. workflows: [],
  296. collections: [],
  297. workflowState: {},
  298. isFirstTime: true,
  299. visitWebTriggers: [],
  300. });
  301. await browser.tabs.create({
  302. active: true,
  303. url: browser.runtime.getURL('newtab.html#/welcome'),
  304. });
  305. return;
  306. }
  307. if (reason === 'update') {
  308. const { workflows } = await browser.storage.local.get('workflows');
  309. const alarmTypes = ['specific-day', 'date', 'interval'];
  310. for (const { trigger, drawflow, id } of workflows) {
  311. let workflowTrigger = trigger?.data || trigger;
  312. if (!trigger) {
  313. const flows = parseJSON(drawflow, drawflow);
  314. workflowTrigger = findTriggerBlock(flows)?.data;
  315. }
  316. const triggerType = workflowTrigger?.type;
  317. if (alarmTypes.includes(triggerType)) {
  318. registerWorkflowTrigger(id, { data: workflowTrigger });
  319. } else if (triggerType === 'context-menu') {
  320. registerContextMenu(id, workflowTrigger);
  321. }
  322. }
  323. }
  324. } catch (error) {
  325. console.error(error);
  326. }
  327. });
  328. browser.runtime.onStartup.addListener(async () => {
  329. const { workflows } = await browser.storage.local.get('workflows');
  330. for (const currWorkflow of workflows) {
  331. let triggerBlock = currWorkflow.trigger;
  332. if (!triggerBlock) {
  333. const flow =
  334. typeof currWorkflow.drawflow === 'string'
  335. ? parseJSON(currWorkflow.drawflow, {})
  336. : currWorkflow.drawflow;
  337. triggerBlock = findTriggerBlock(flow)?.data;
  338. }
  339. if (triggerBlock) {
  340. if (triggerBlock.type === 'specific-day') {
  341. const alarm = await browser.alarms.get(currWorkflow.id);
  342. if (!alarm) await registerSpecificDay(currWorkflow.id, triggerBlock);
  343. } else if (triggerBlock.type === 'date' && triggerBlock.date) {
  344. const [hour, minute] = triggerBlock.time.split(':');
  345. const date = dayjs(triggerBlock.date)
  346. .hour(hour)
  347. .minute(minute)
  348. .second(0);
  349. const isBefore = dayjs().isBefore(date);
  350. if (isBefore) {
  351. await browser.alarms.create(currWorkflow.id, {
  352. when: date.valueOf(),
  353. });
  354. }
  355. } else if (triggerBlock.type === 'on-startup') {
  356. workflow.execute(currWorkflow);
  357. }
  358. }
  359. }
  360. });
  361. const message = new MessageListener('background');
  362. message.on('fetch:text', (url) => {
  363. return fetch(url).then((response) => response.text());
  364. });
  365. message.on('open:dashboard', async (url) => {
  366. await openDashboard(url);
  367. return Promise.resolve(true);
  368. });
  369. message.on('set:active-tab', (tabId) => {
  370. return browser.tabs.update(tabId, { active: true });
  371. });
  372. message.on('debugger:send-command', ({ tabId, method, params }) => {
  373. return new Promise((resolve) => {
  374. chrome.debugger.sendCommand({ tabId }, method, params, resolve);
  375. });
  376. });
  377. message.on('get:sender', (_, sender) => sender);
  378. message.on('get:file', (path) => getFile(path));
  379. message.on('get:tab-screenshot', (options) =>
  380. browser.tabs.captureVisibleTab(options)
  381. );
  382. message.on('collection:execute', (collection) => {
  383. const engine = new CollectionEngine(collection, {
  384. states: workflow.states,
  385. logger: workflow.logger,
  386. });
  387. engine.init();
  388. });
  389. message.on('workflow:execute', (workflowData, sender) => {
  390. if (workflowData.includeTabId) {
  391. if (!workflowData.options) workflowData.options = {};
  392. workflowData.options.tabId = sender.tab.id;
  393. }
  394. workflow.execute(workflowData, workflowData?.options || {});
  395. });
  396. message.on('workflow:stop', (id) => workflow.states.stop(id));
  397. browser.runtime.onMessage.addListener(message.listener());