// ==UserScript==
// @name Facebook Business Automation
// @namespace http://tampermonkey.net/
// @version 2.8
// @description Automates actions in Facebook Business Manager, specifically Rights Manager.
// @author You
// @match https://business.facebook.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license MIT
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==
(function () {
'use strict';
// Function to get element by XPath
function getElementByXpath(path) {
return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
// Utility function to wait for an element to appear in the DOM
function waitForElement(selector, timeoutSeconds = 15) {
return new Promise((resolve, reject) => {
const intervalTime = 500; // Check every 500ms
let elapsedTime = 0;
const interval = setInterval(() => {
const element = typeof selector === 'string' ? document.querySelector(selector) : selector();
if (element) {
clearInterval(interval); // Stop checking
resolve(element); // Resolve with the found element
} else if (elapsedTime >= timeoutSeconds * 1000) {
clearInterval(interval); // Stop checking after timeout
reject(new Error(`Element not found: ${selector}`));
}
elapsedTime += intervalTime;
}, intervalTime);
});
}
// Function to click on an element using XPath and return a promise
function clickElementByXPath(xpath) {
return new Promise((resolve, reject) => {
const element = getElementByXpath(xpath);
if (element) {
element.click();
console.log(`Clicked element with XPath: ${xpath}`);
resolve(true);
} else {
console.log(`Element with XPath not found: ${xpath}`);
reject(new Error(`Element not found: ${xpath}`));
}
});
}
// Function to simulate typing into a textarea
function simulateTyping(element, text) {
return new Promise(resolve => {
let index = 0;
const typingInterval = 50; // Adjust typing speed here (milliseconds per character)
function typeNextCharacter() {
if (index < text.length) {
const char = text[index];
const keyCode = char.charCodeAt(0);
// Trigger keydown event
const keydownEvent = new KeyboardEvent('keydown', {
key: char,
code: `Key${char.toUpperCase()}`,
which: keyCode,
keyCode: keyCode,
bubbles: true,
cancelable: true
});
element.dispatchEvent(keydownEvent);
// Trigger keypress event
const keypressEvent = new KeyboardEvent('keypress', {
key: char,
code: `Key${char.toUpperCase()}`,
which: keyCode,
keyCode: keyCode,
bubbles: true,
cancelable: true
});
element.dispatchEvent(keypressEvent);
// Update the textarea value
element.value += char;
element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
// Trigger keyup event
const keyupEvent = new KeyboardEvent('keyup', {
key: char,
code: `Key${char.toUpperCase()}`,
which: keyCode,
keyCode: keyCode,
bubbles: true,
cancelable: true
});
element.dispatchEvent(keyupEvent);
index++;
setTimeout(typeNextCharacter, typingInterval);
} else {
// All characters have been typed
element.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
resolve();
}
}
typeNextCharacter();
});
}
// Function to insert text using execCommand
function insertTextWithExecCommand(textarea, text) {
textarea.focus();
try {
const success = document.execCommand('insertText', false, text);
if (!success) {
console.warn("execCommand('insertText') failed, falling back to simulateTyping");
simulateTyping(textarea, text); // Use simulateTyping as a fallback
}
} catch (err) {
console.error("Error using execCommand, falling back to simulateTyping:", err);
simulateTyping(textarea, text); // Use simulateTyping as a fallback
}
// Dispatch necessary events
textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
}
// Function to find element by style attribute
function findElementByStyle(styleAttribute, styleValue) {
// Get all elements in the document
const allElements = document.getElementsByTagName('*');
// Iterate over all elements
for (let element of allElements) {
// Check if the element has the specified style attribute and value
if (element.style[styleAttribute] === styleValue) {
return element;
}
}
return null;
}
function enterTextInTextarea(textareaPlaceholder, text) {
return new Promise((resolve) => {
// Query the textarea using the placeholder attribute
const textarea = document.querySelector(`textarea[placeholder="${textareaPlaceholder}"]`);
if (textarea) {
console.log(`Found target textarea with placeholder: ${textareaPlaceholder}`);
// Clear existing text reliably
textarea.value = '';
textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
// Insert text using execCommand
insertTextWithExecCommand(textarea, text);
console.log(`Entered text into textarea with placeholder: ${textareaPlaceholder}`);
setTimeout(() => { // ADDED WAIT TIME HERE
resolve();
}, 6000);
} else {
console.warn(`Target textarea not found, moving on...`);
resolve(); // Resolve without error to move on
}
});
}
// Function to check and click "Confirm my ownership" and handle dispute submission
function checkAndClickConfirmOwnership() {
const actionDivSelector = 'div[aria-labelledby][role="menuitem"]';
const nextItemXPath = "/html[1]/body[1]/span[1]/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]/span[1]/div[1]/div[2]/div[1]/div[1]";
const chooseActionXPath1 = "/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]";
const chooseActionXPath2 = "/html/body/span/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]/span/div/div/div[1]";
const disputeTextareaName = 'Add details about your ownership'; // Use the name attribute
const disputeText = "Per the supplied composition and CWR Data, West One Music Group is the owner of this sound recording.";
const submitDisputeXPath = "/html[1]/body[1]/span[1]/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[2]/div[1]/div[2]/div[1]/span[1]/div[1]/div[1]/div[1]";
function tryConfirmOwnership() {
waitForElement(actionDivSelector)
.then(() => {
const actionDivs = document.querySelectorAll(actionDivSelector);
let ownershipFound = false;
for (let i = 0; i < actionDivs.length; i++) {
const menuItemText = actionDivs[i].textContent;
if (menuItemText.includes("Confirm My Ownership")) {
actionDivs[i].click();
console.log("Clicked 'Confirm my ownership'");
ownershipFound = true;
setTimeout(() => { // ADDED WAIT TIME HERE
// Enter text, submit dispute, confirm, then move to next item
enterTextInTextarea(disputeTextareaName, disputeText)
.then(() => {
console.log("Waiting for 5 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 6000)); // 5-second delay
})
.then(() => clickElementByXPath(submitDisputeXPath))
.then(() => {
console.log("Added 9-second delay after submitting dispute");
return new Promise(resolve => setTimeout(resolve, 7000)); // 9-second delay
})
.then(() => clickConfirmButton()) // Click the confirm button
.then(() => {
console.log("Waiting for 5 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 6000)); // 5-second delay
})
.then(() => {
console.log("Submitted dispute and confirmed. Moving to next item.");
// Add a 5-second wait here
setTimeout(function () {
// Move to the next item and loop indefinitely
clickElementByXPath(nextItemXPath)
.then(() => clickElementByXPath(chooseActionXPath1))
.catch(() => clickElementByXPath(chooseActionXPath2)) // Fallback to the second XPath
.then(() => {
console.log("Waiting for 4 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 4000)); // 5-second delay
})
.then(() => tryConfirmOwnership())
.catch(error => {
console.error("Error moving to next item:", error);
console.log("Retrying from beginning...");
setTimeout(resumeAutomation, 5000);
});
}, 5000); // 5000 milliseconds = 5 seconds
})
.catch(error => { // This is the important part - catching the rejection from enterTextInTextarea
console.error("Error during enterTextInTextarea or subsequent actions:", error);
console.log("Ignoring error and moving on...");
// Continue with the next steps regardless of the error
clickElementByXPath(nextItemXPath)
.then(() => clickElementByXPath(chooseActionXPath1))
.catch(() => clickElementByXPath(chooseActionXPath2)) // Fallback to the second XPath
.then(() => {
console.log("Waiting for 4 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 4000)); // 5-second delay
})
.then(() => tryConfirmOwnership())
.catch(error => {
console.error("Error moving to next item:", error);
console.log("Retrying from beginning...");
setTimeout(resumeAutomation, 5000);
});
});
}, 6000);
break; // Exit the loop once "Confirm my ownership" is found and clicked
}
}
if (!ownershipFound) {
console.log("Confirm my ownership not available. Moving to the next item.");
// Move to the next item if "Confirm my ownership" is not available
clickElementByXPath(nextItemXPath)
.then(() => {
console.log("Waiting for 5 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
})
.then(() => clickElementByXPath(chooseActionXPath1))
.catch(() => clickElementByXPath(chooseActionXPath2)) // Fallback to the second XPath
.then(() => {
console.log("Waiting for 5 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
})
.then(() => tryConfirmOwnership())
.catch(error => {
console.error("Error moving to next item:", error);
});
}
})
.catch(error => {
console.error("Error in checkAndClickConfirmOwnership:", error);
});
}
tryConfirmOwnership();
}
function clickConfirmButton() {
return new Promise((resolve, reject) => {
const confirmButton = $("div:contains('Confirm')");
if (confirmButton.length > 0) {
confirmButton.click();
console.log("Clicked confirm button using jQuery");
resolve(true);
} else {
console.log("Confirm button not found using jQuery");
reject(new Error("Confirm button not found"));
}
});
}
function navigateAndLog() {
GM_setValue('automationActive', true);
window.location.href = 'https://business.facebook.com/latest/rights_manager/rights_manager_action_items?asset_id=114882491608990';
}
// Function to wait for a non-empty first row in <tbody>
function waitForNonEmptyFirstRow(selector, timeoutSeconds = 30) {
return new Promise((resolve, reject) => {
const intervalTime = 500; // Check every 500ms
let elapsedTime = 0;
const interval = setInterval(() => {
const firstRow = document.querySelector(selector);
if (firstRow && firstRow.textContent.trim() !== '') {
clearInterval(interval); // Stop checking when non-empty row is found
resolve(firstRow);
} else if (elapsedTime >= timeoutSeconds * 1000) {
clearInterval(interval); // Stop checking after timeout
reject(new Error(`Non-empty first row not found in: ${selector}`));
}
elapsedTime += intervalTime;
}, intervalTime);
});
}
function resumeAutomation() {
const automationActive = GM_getValue('automationActive', false);
if (automationActive) {
console.log('Resuming automation...');
waitForNonEmptyFirstRow('tbody tr:first-child', 30)
.then((firstRow) => {
console.log('First item in <tbody>:', firstRow.textContent.trim());
setTimeout(() => {
const checkbox = firstRow.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.click();
console.log('Checkbox clicked!');
setTimeout(() => {
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";
clickElementByXPath(see_details_xpath)
.then(() => {
console.log("Clicked 'See Details', waiting for 'Choose Action'...");
return waitForElement(() => findChooseActionDiv(), 15);
})
.then(() => {
console.log("Waiting for 5 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
})
.then(() => {
console.log("'Choose Action' element found, clicking...");
return findAndClickChooseActionDiv();
})
.then(() => {
console.log("Waiting for 5 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
})
.then(() => {
console.log("Clicked 'Choose Action'");
setTimeout(checkAndClickConfirmOwnership, 3000);
})
.then(() => {
console.log("Waiting for 5 seconds before submitting dispute...");
return new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay
})
.catch(error => {
console.error("Error in automation sequence:", error);
});
}, 5000);
} else {
console.warn('Checkbox not found in the first row.');
}
GM_setValue('automationActive', false);
}, 7000);
})
.catch((error) => {
console.error(error.message);
console.log("Retrying to find a non-empty first row...");
resumeAutomation(); // Retry until a non-empty row is found
});
}
}
function findChooseActionDiv() {
const divs = document.querySelectorAll('div');
for (let div of divs) {
if (div.textContent.trim() === "Choose action") {
return div;
}
}
return null;
}
function findAndClickChooseActionDiv() {
return new Promise((resolve, reject) => {
const div = findChooseActionDiv();
if (div) {
div.click();
console.log("Clicked 'Choose action' div");
resolve();
} else {
reject(new Error("'Choose action' div not found"));
}
});
}
// Add Start button
const startButton = document.createElement('button');
startButton.textContent = 'Start';
startButton.style.position = 'fixed';
startButton.style.top = '20px';
startButton.style.left = '20px';
startButton.style.zIndex = '1000';
startButton.style.backgroundColor = '#4267B2';
startButton.style.color = 'white';
startButton.style.border = 'none';
startButton.style.padding = '10px 20px';
startButton.style.cursor = 'pointer';
startButton.style.borderRadius = '5px';
startButton.addEventListener('click', navigateAndLog);
document.body.appendChild(startButton);
resumeAutomation();
})();