Facebook Business Automation

Automates actions in Facebook Business Manager, specifically Rights Manager.

  1. // ==UserScript==
  2. // @name Facebook Business Automation
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.9
  5. // @description Automates actions in Facebook Business Manager, specifically Rights Manager.
  6. // @author You
  7. // @match https://business.facebook.com/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_addStyle
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // Function to get element by XPath
  18. function getElementByXpath(path) {
  19. return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  20. }
  21.  
  22. // Utility function to wait for an element to appear in the DOM
  23. function waitForElement(selector, timeoutSeconds = 15) {
  24. return new Promise((resolve, reject) => {
  25. const intervalTime = 500; // Check every 500ms
  26. let elapsedTime = 0;
  27.  
  28. const interval = setInterval(() => {
  29. const element = typeof selector === 'string' ? document.querySelector(selector) : selector();
  30. if (element) {
  31. clearInterval(interval); // Stop checking
  32. resolve(element); // Resolve with the found element
  33. } else if (elapsedTime >= timeoutSeconds * 1000) {
  34. clearInterval(interval); // Stop checking after timeout
  35. reject(new Error(`Element not found: ${selector}`));
  36. }
  37. elapsedTime += intervalTime;
  38. }, intervalTime);
  39. });
  40. }
  41.  
  42. // Function to click on an element using XPath and return a promise
  43. function clickElementByXPath(xpath) {
  44. return new Promise((resolve, reject) => {
  45. const element = getElementByXpath(xpath);
  46. if (element) {
  47. element.click();
  48. console.log(`Clicked element with XPath: ${xpath}`);
  49. resolve(true);
  50. } else {
  51. console.log(`Element with XPath not found: ${xpath}`);
  52. reject(new Error(`Element not found: ${xpath}`));
  53. }
  54. });
  55. }
  56.  
  57. // Function to simulate typing into a textarea
  58. function simulateTyping(element, text) {
  59. return new Promise(resolve => {
  60. let index = 0;
  61. const typingInterval = 50; // Adjust typing speed here (milliseconds per character)
  62.  
  63. function typeNextCharacter() {
  64. if (index < text.length) {
  65. const char = text[index];
  66. const keyCode = char.charCodeAt(0);
  67.  
  68. // Trigger keydown event
  69. const keydownEvent = new KeyboardEvent('keydown', {
  70. key: char,
  71. code: `Key${char.toUpperCase()}`,
  72. which: keyCode,
  73. keyCode: keyCode,
  74. bubbles: true,
  75. cancelable: true
  76. });
  77. element.dispatchEvent(keydownEvent);
  78.  
  79. // Trigger keypress event
  80. const keypressEvent = new KeyboardEvent('keypress', {
  81. key: char,
  82. code: `Key${char.toUpperCase()}`,
  83. which: keyCode,
  84. keyCode: keyCode,
  85. bubbles: true,
  86. cancelable: true
  87. });
  88. element.dispatchEvent(keypressEvent);
  89.  
  90. // Update the textarea value
  91. element.value += char;
  92. element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
  93.  
  94. // Trigger keyup event
  95. const keyupEvent = new KeyboardEvent('keyup', {
  96. key: char,
  97. code: `Key${char.toUpperCase()}`,
  98. which: keyCode,
  99. keyCode: keyCode,
  100. bubbles: true,
  101. cancelable: true
  102. });
  103. element.dispatchEvent(keyupEvent);
  104.  
  105. index++;
  106. setTimeout(typeNextCharacter, typingInterval);
  107. } else {
  108. // All characters have been typed
  109. element.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
  110. resolve();
  111. }
  112. }
  113.  
  114. typeNextCharacter();
  115. });
  116. }
  117.  
  118. // Function to insert text using execCommand
  119. function insertTextWithExecCommand(textarea, text) {
  120. textarea.focus();
  121. try {
  122. const success = document.execCommand('insertText', false, text);
  123. if (!success) {
  124. console.warn("execCommand('insertText') failed, falling back to simulateTyping");
  125. simulateTyping(textarea, text); // Use simulateTyping as a fallback
  126. }
  127. } catch (err) {
  128. console.error("Error using execCommand, falling back to simulateTyping:", err);
  129. simulateTyping(textarea, text); // Use simulateTyping as a fallback
  130. }
  131. // Dispatch necessary events
  132. textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
  133. textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
  134. }
  135.  
  136. function scrollToTextareaAndEnterText(containerXPath, textareaXPath, text, maxScrollAttempts = 10) {
  137. return new Promise((resolve, reject) => {
  138. const container = getElementByXpath(containerXPath);
  139. if (!container) {
  140. return reject(new Error(`Scrollable container not found: ${containerXPath}`));
  141. }
  142.  
  143. let attempts = 0;
  144.  
  145. function tryScroll() {
  146. // Query the textarea directly using the placeholder attribute.
  147. const textarea = getElementByXpath(textareaXPath);
  148.  
  149. if (textarea) {
  150. console.log(`Found target textarea with XPath: ${textareaXPath}`);
  151.  
  152. // Clear existing text reliably
  153. textarea.value = '';
  154. textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
  155.  
  156. // Insert text using execCommand
  157. insertTextWithExecCommand(textarea, text);
  158. console.log(`Entered text into textarea with XPath: ${textareaXPath}`);
  159. setTimeout(() => { // ADDED WAIT TIME HERE
  160. resolve();
  161. }, 6000);
  162.  
  163. } else if (attempts < maxScrollAttempts) {
  164. container.scrollBy(0, 300); // Scroll down by 300px
  165. attempts++;
  166. console.log(`Scrolling attempt ${attempts}...`);
  167. setTimeout(tryScroll, 500); // Retry after a short delay
  168. } else {
  169. reject(new Error(`Target textarea not found after ${maxScrollAttempts} scrolling attempts.`));
  170. }
  171. }
  172.  
  173. tryScroll();
  174. });
  175. }
  176.  
  177. // Function to check and click "Confirm my ownership" and handle dispute submission
  178. function checkAndClickConfirmOwnership() {
  179. const actionDivSelector = 'div[aria-labelledby][role="menuitem"]';
  180. const nextItemXPath = "/html[1]/body[1]/span[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[4]/div[1]/div[1]/div[1]/div[2]/div[1]/div[1]";
  181. const chooseActionXPath = "/html/body/span[2]/div/div[1]/div[1]/div/div/div/div/div/div[3]/div/div[1]/div/div/div/div/div/div/div/div/div[1]/div[2]/div/div/div[2]/div/div/div/div[2]/div/div/div/div[2]/div[1]";
  182.  
  183. const disputeTextareaXPath = "/html[1]/body[1]/span[2]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[3]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[2]/div[1]/div[1]/div[2]/div[1]/div[3]/div[3]/div[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]/div[1]/div[2]/div[1]/div[1]/div[1]/div[1]/div[2]/textarea[1]";
  184. const disputeText = "Per the supplied composition and CWR Data, West One Music Group is the owner of this sound recording.";
  185. const submitDisputeXPath = "/html/body/span[2]/div/div[1]/div[1]/div/div/div/div/div/div[3]/div/div[1]/div/div/div/div/div/div/div/div[2]/div/div[2]/div/span/div/div/div";
  186. const confirmButtonXPath = "/html/body/div[6]/div[1]/div[1]/div/div/div/div/div[3]/div/div[2]/div/span/div/div/div"; // Confirm button after Submit Dispute
  187. const scrollableContainerXPath = "/html/body/span[2]/div/div[1]/div[1]/div/div/div/div/div/div[3]/div/div[1]/div/div/div/div/div/div/div/div[1]/div[1]";
  188.  
  189. function tryConfirmOwnership() {
  190. waitForElement(actionDivSelector)
  191. .then(() => {
  192. const actionDivs = document.querySelectorAll(actionDivSelector);
  193. for (let i = 0; i < actionDivs.length; i++) {
  194. if (actionDivs[i].textContent.includes("Confirm my ownership")) {
  195. actionDivs[i].click();
  196. console.log("Clicked 'Confirm my ownership'");
  197. setTimeout(() => { // ADDED WAIT TIME HERE
  198. // Scroll to textarea, enter text, submit dispute, confirm, then move to next item
  199. scrollToTextareaAndEnterText(scrollableContainerXPath, disputeTextareaXPath, disputeText)
  200. .then(() => clickElementByXPath(submitDisputeXPath))
  201. .then(() => {
  202. console.log("Added 9-second delay after submitting dispute");
  203. return new Promise(resolve => setTimeout(resolve, 7000)); // 9-second delay
  204. })
  205. // .then(() => waitForElement(() => getElementByXpath(confirmButtonXPath), 15)) // Wait for the confirm button to appear
  206. .then(() => clickElementByXPath(confirmButtonXPath)) // Click the confirm button
  207. .then(() => {
  208. console.log("Waiting for 5 seconds before submitting dispute...");
  209. return new Promise(resolve => setTimeout(resolve, 6000)); // 5-second delay
  210. })
  211. .then(() => {
  212. console.log("Submitted dispute and confirmed. Moving to next item.");
  213. // Add a 5-second wait here
  214. setTimeout(function () {
  215. // Move to the next item and loop indefinitely
  216. clickElementByXPath(nextItemXPath)
  217. .then(() => clickElementByXPath(chooseActionXPath))
  218. .then(() => {
  219. console.log("Waiting for 4 seconds before submitting dispute...");
  220. return new Promise(resolve => setTimeout(resolve, 4000)); // 5-second delay
  221. })
  222. .then(() => tryConfirmOwnership())
  223. .catch(error => {
  224. console.error("Error moving to next item:", error);
  225. console.log("Retrying from beginning...");
  226. setTimeout(resumeAutomation, 5000);
  227. });
  228. }, 5000); // 5000 milliseconds = 5 seconds
  229. })
  230. .catch(error => { // This is the important part - catching the rejection from scrollToTextareaAndEnterText
  231. console.error("Error during scrollToTextareaAndEnterText or subsequent actions:", error);
  232. console.log("Retrying from beginning...");
  233. setTimeout(resumeAutomation, 5000);
  234. });
  235. }, 6000);
  236.  
  237. return; // Stop looking for "Confirm my ownership" in this iteration
  238. }
  239. }
  240. console.log("Confirm my ownership not available");
  241. // Move to the next item if "Confirm my ownership" is not available
  242. clickElementByXPath(nextItemXPath)
  243. .then(() => {
  244. console.log("Waiting for 5 seconds before submitting dispute...");
  245. return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
  246. })
  247. .then(() => clickElementByXPath(chooseActionXPath))
  248. .then(() => {
  249. console.log("Waiting for 5 seconds before submitting dispute...");
  250. return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
  251. })
  252. .then(() => tryConfirmOwnership())
  253. .catch(error => {
  254. console.error("Error moving to next item:", error);
  255. });
  256. })
  257. .catch(error => {
  258. console.error("Error in checkAndClickConfirmOwnership:", error);
  259. });
  260. }
  261.  
  262. tryConfirmOwnership();
  263. }
  264.  
  265. function navigateAndLog() {
  266. GM_setValue('automationActive', true);
  267. window.location.href = 'https://business.facebook.com/latest/rights_manager/rights_manager_action_items?asset_id=114882491608990';
  268. }
  269.  
  270. // Function to wait for a non-empty first row in <tbody> using MutationObserver
  271. function waitForFirstRowInTbody(tbodySelector, callback, timeoutSeconds = 30) {
  272. const tbody = document.querySelector(tbodySelector);
  273. if (!tbody) {
  274. console.error('Tbody not found:', tbodySelector);
  275. return;
  276. }
  277.  
  278. const observer = new MutationObserver((mutations) => {
  279. for (const mutation of mutations) {
  280. if (mutation.type === 'childList') {
  281. const firstRow = tbody.querySelector('tr:first-child');
  282. if (firstRow && firstRow.textContent.trim() !== '') {
  283. observer.disconnect(); // Stop observing once the row is found
  284. callback(firstRow);
  285. return;
  286. }
  287. }
  288. }
  289. });
  290.  
  291. observer.observe(tbody, { childList: true, subtree: true });
  292.  
  293. // Optional: Add a timeout to stop observing after a certain period
  294. setTimeout(() => {
  295. observer.disconnect();
  296. console.error('Timeout: First row not found within the specified time.');
  297. }, timeoutSeconds * 1000);
  298. }
  299.  
  300. function resumeAutomation() {
  301. const automationActive = GM_getValue('automationActive', false);
  302.  
  303. if (automationActive) {
  304. console.log('Resuming automation...');
  305.  
  306. // Wait for 7 seconds before proceeding
  307. setTimeout(() => {
  308. console.log("Waited for 7 seconds before submitting dispute...");
  309.  
  310. waitForFirstRowInTbody('tbody', (firstRow) => {
  311. console.log('First item in <tbody>:', firstRow.textContent.trim());
  312.  
  313. setTimeout(() => {
  314. const checkbox = firstRow.querySelector('input[type="checkbox"]');
  315. if (checkbox) {
  316. checkbox.click();
  317. console.log('Checkbox clicked!');
  318.  
  319. setTimeout(() => {
  320. const see_details_xpath = "/html/body/div[1]/div/div[1]/div/div[2]/div/div/div[1]/span/div/div/div[1]/div[1]/div/div/div/div/div/div/div/div/div/div[2]/div[2]/div/div/div/div/div[1]/div[2]/span[2]/div[8]/div[1]/div/div[3]/div[2]/div[1]/div";
  321. const choose_action_xpath = "/html/body/span[2]/div/div[1]/div[1]/div/div/div/div/div/div[3]/div/div[1]/div/div/div/div/div/div/div/div/div[1]/div[2]/div/div/div[2]/div/div/div/div[2]/div/div/div/div[2]/div[1]";
  322.  
  323. clickElementByXPath(see_details_xpath)
  324. .then(() => {
  325. console.log("Clicked 'See Details', waiting for 'Choose Action'...");
  326. return waitForElement(() => getElementByXpath(choose_action_xpath), 15);
  327. })
  328. .then(() => {
  329. console.log("Waiting for 5 seconds before submitting dispute...");
  330. return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
  331. })
  332. .then(() => {
  333. console.log("'Choose Action' element found, clicking...");
  334. return clickElementByXPath(choose_action_xpath);
  335. })
  336. .then(() => {
  337. console.log("Waiting for 5 seconds before submitting dispute...");
  338. return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
  339. })
  340. .then(() => {
  341. console.log("Clicked 'Choose Action'");
  342. setTimeout(checkAndClickConfirmOwnership, 3000);
  343. })
  344. .then(() => {
  345. console.log("Waiting for 5 seconds before submitting dispute...");
  346. return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
  347. })
  348. .catch(error => {
  349. console.error("Error in automation sequence:", error);
  350. });
  351. }, 5000);
  352. } else {
  353. console.warn('Checkbox not found in the first row.');
  354. }
  355.  
  356. GM_setValue('automationActive', false);
  357. }, 7000);
  358. });
  359. }, 7000); // 7-second delay
  360. }
  361. }
  362.  
  363. // Add Start button
  364. const startButton = document.createElement('button');
  365. startButton.textContent = 'Start';
  366. startButton.style.position = 'fixed';
  367. startButton.style.top = '20px';
  368. startButton.style.left = '20px';
  369. startButton.style.zIndex = '1000';
  370. startButton.style.backgroundColor = '#4267B2';
  371. startButton.style.color = 'white';
  372. startButton.style.border = 'none';
  373. startButton.style.padding = '10px 20px';
  374. startButton.style.cursor = 'pointer';
  375. startButton.style.borderRadius = '5px';
  376.  
  377. startButton.addEventListener('click', navigateAndLog);
  378. document.body.appendChild(startButton);
  379.  
  380. resumeAutomation();
  381. })();