Last active
December 1, 2025 09:55
-
-
Save dvygolov/3d7727b0544cd94531020ef599925900 to your computer and use it in GitHub Desktop.
Script for import/export Facebook Ads Autorules
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Constants | |
| const currentVersion = "2025.12.01"; | |
| // Global variable to store all accounts with rule counts | |
| let allAccountsData = []; | |
| // Global UI instance for logging | |
| let uiInstance = null; | |
| // Helper function to log messages | |
| function logMessage(message, type = "info") { | |
| if (uiInstance && uiInstance.log) { | |
| uiInstance.log(message, type); | |
| } | |
| // Also log to console for debugging | |
| if (type === "error") { | |
| console.error(message); | |
| } else { | |
| console.log(message); | |
| } | |
| } | |
| const CURRENCY_FIELDS = [ | |
| "spent", | |
| "today_spent", | |
| "cost_per_purchase_fb", | |
| "cost_per_add_to_cart_fb", | |
| "cost_per_complete_registration_fb", | |
| "cost_per_view_content_fb", | |
| "cost_per_search_fb", | |
| "cost_per_initiate_checkout_fb", | |
| "cost_per_lead_fb", | |
| "cost_per_add_payment_info_fb", | |
| "cost_per_link_click", | |
| "cpc", | |
| "cpm" | |
| ]; | |
| const CURRENCY_OFFSETS = { | |
| "DZD": 100, | |
| "ARS": 100, | |
| "AUD": 100, | |
| "BHD": 100, | |
| "BDT": 100, | |
| "BOB": 100, | |
| "BGN": 100, | |
| "BRL": 100, | |
| "GBP": 100, | |
| "CAD": 100, | |
| "CLP": 1, | |
| "CNY": 100, | |
| "COP": 1, | |
| "CRC": 1, | |
| "HRK": 100, | |
| "CZK": 100, | |
| "DKK": 100, | |
| "EGP": 100, | |
| "EUR": 100, | |
| "GTQ": 100, | |
| "HNL": 100, | |
| "HKD": 100, | |
| "HUF": 1, | |
| "ISK": 1, | |
| "INR": 100, | |
| "IDR": 1, | |
| "ILS": 100, | |
| "JPY": 1, | |
| "JOD": 100, | |
| "KES": 100, | |
| "KRW": 1, | |
| "LVL": 100, | |
| "LTL": 100, | |
| "MOP": 100, | |
| "MYR": 100, | |
| "MXN": 100, | |
| "NZD": 100, | |
| "NIO": 100, | |
| "NGN": 100, | |
| "NOK": 100, | |
| "PKR": 100, | |
| "PYG": 1, | |
| "PEN": 100, | |
| "PHP": 100, | |
| "PLN": 100, | |
| "QAR": 100, | |
| "RON": 100, | |
| "RUB": 100, | |
| "SAR": 100, | |
| "RSD": 100, | |
| "SGD": 100, | |
| "SKK": 100, | |
| "ZAR": 100, | |
| "SEK": 100, | |
| "CHF": 100, | |
| "TWD": 1, | |
| "THB": 100, | |
| "TRY": 100, | |
| "AED": 100, | |
| "UAH": 100, | |
| "USD": 100, | |
| "UYU": 100, | |
| "VEF": 100, | |
| "VND": 1, | |
| "FBZ": 100, | |
| "VES": 100 | |
| }; | |
| class FbApi { | |
| apiUrl = "https://adsmanager-graph.facebook.com/v23.0/"; | |
| async getRequest(path, qs = null, token = null) { | |
| token = token ?? __accessToken; | |
| let finalUrl = path.startsWith('http') ? path : this.apiUrl+path; | |
| // Check if URL already contains access_token (e.g., from pagination) | |
| const hasAccessToken = finalUrl.includes('access_token='); | |
| // Only add access_token if not already present | |
| if (!hasAccessToken) { | |
| qs = qs != null ? `${qs}&access_token=${token}` : `access_token=${token}`; | |
| const separator = finalUrl.includes('?') ? '&' : '?'; | |
| finalUrl = `${finalUrl}${separator}${qs}`; | |
| } else if (qs) { | |
| // URL has access_token but we still need to append other params | |
| finalUrl = `${finalUrl}&${qs}`; | |
| } | |
| let f = await fetch(finalUrl, { | |
| headers: { | |
| accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", | |
| "accept-language": "ca-ES,ca;q=0.9,en-US;q=0.8,en;q=0.7", | |
| "cache-control": "max-age=0", | |
| "sec-ch-ua": '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"', | |
| "sec-ch-ua-mobile": "?0", | |
| "sec-ch-ua-platform": '"Windows"', | |
| "sec-fetch-dest": "empty", | |
| "sec-fetch-mode": "cors", | |
| "sec-fetch-site": "same-site", | |
| }, | |
| referrerPolicy: "strict-origin-when-cross-origin", | |
| body: null, | |
| method: "GET", | |
| mode: "cors", | |
| credentials: "include", | |
| referrer: "https://business.facebook.com/", | |
| referrerPolicy: "origin-when-cross-origin", | |
| }); | |
| let js = await f.json(); | |
| return js; | |
| } | |
| async getAllPages(path, qs, token = null) { | |
| let items = []; | |
| let page = await this.getRequest(path, qs, token); | |
| items = items.concat(page.data); | |
| let i = 2; | |
| while (page.paging && page.paging.next) { | |
| page = await this.getRequest(page.paging.next, null, token); | |
| items = items.concat(page.data); | |
| i++; | |
| } | |
| return items; | |
| } | |
| async postRequest(path, body, token = null) { | |
| token = token ?? __accessToken; | |
| body["access_token"] = token; | |
| let headers = { | |
| accept: "*/*", | |
| "accept-language": "en-US,en;q=0.9", | |
| "content-type": "application/x-www-form-urlencoded", | |
| "sec-ch-ua": '"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"', | |
| "sec-ch-ua-mobile": "?0", | |
| "sec-ch-ua-platform": '"Windows"', | |
| "sec-fetch-dest": "empty", | |
| "sec-fetch-mode": "cors", | |
| "sec-fetch-site": "same-site", | |
| }; | |
| let finalUrl = path.startsWith('http') ? path : this.apiUrl+path; | |
| let f = await fetch(finalUrl, { | |
| headers: headers, | |
| referrer: "https://business.facebook.com/", | |
| referrerPolicy: "origin-when-cross-origin", | |
| body: new URLSearchParams(body).toString(), | |
| method: "POST", | |
| mode: "cors", | |
| credentials: "include", | |
| }); | |
| let json = await f.json(); | |
| return json; | |
| } | |
| } | |
| class FbRules { | |
| fb = new FbApi(); | |
| async getAllRules(accountId) { | |
| const allRules = await this.fb.getAllPages(`act_${accountId}/adrules_library`, "fields=id,name,evaluation_spec,execution_spec,schedule_spec,status&limit=100"); | |
| return { data: allRules }; | |
| } | |
| async clearRules(accountId) { | |
| let rules = await this.getAllRules(accountId); | |
| let rulesCount = rules.data.length; | |
| if (rulesCount == 0) return; | |
| console.log(`Deleting ${rulesCount} rules.`); | |
| for (let i = 0; i < rules.data.length; i++) { | |
| const rule = rules.data[i]; | |
| console.log(`Deleting rule ${JSON.stringify(rule)}...`); | |
| console.log(await this.delRule(rule["id"])); | |
| // Add delay between deletions to avoid rate limiting (300ms) | |
| if (i < rules.data.length - 1) { | |
| await new Promise(resolve => setTimeout(resolve, 300)); | |
| } | |
| } | |
| } | |
| async addRule(accountId, name, evalSpec, execSpec, schedSpec) { | |
| let body = { | |
| locale: "en_US", | |
| evaluation_spec: JSON.stringify(evalSpec), | |
| execution_spec: JSON.stringify(execSpec), | |
| name: name, | |
| schedule_spec: JSON.stringify(schedSpec), | |
| status: "ENABLED", | |
| }; | |
| return await this.fb.postRequest(`act_${accountId}/adrules_library`, body); | |
| } | |
| async delRule(ruleId) { | |
| let body = { method: "delete" }; | |
| return await this.fb.postRequest(`${ruleId}?method=delete`, body); | |
| } | |
| async execRule(ruleId) { | |
| let body = { | |
| method: "post", | |
| locale: "en_US", | |
| }; | |
| return await this.fb.postRequest(`${ruleId}/execute?method=post`, body); | |
| } | |
| } | |
| class FileSelector { | |
| constructor(fileProcessor) { | |
| this.fileProcessor = fileProcessor; | |
| } | |
| createDiv() { | |
| this.div = document.createElement("div"); | |
| this.div.style.position = "fixed"; | |
| this.div.style.top = "50%"; | |
| this.div.style.left = "50%"; | |
| this.div.style.transform = "translate(-50%, -50%)"; | |
| this.div.style.width = "200px"; | |
| this.div.style.height = "120px"; | |
| this.div.style.backgroundColor = "yellow"; | |
| this.div.style.zIndex = "1000"; | |
| this.div.style.display = "flex"; | |
| this.div.style.flexDirection = "column"; | |
| this.div.style.alignItems = "center"; | |
| this.div.style.justifyContent = "center"; | |
| this.div.style.padding = "10px"; | |
| this.div.style.boxSizing = "border-box"; | |
| this.div.style.borderRadius = "10px"; | |
| // Create and style the title | |
| var title = document.createElement("div"); | |
| title.innerHTML = "Select file to import autorules"; | |
| title.style.textAlign = "center"; | |
| title.style.fontWeight = "bold"; | |
| // Create and style the close button | |
| var closeButton = document.createElement("button"); | |
| closeButton.innerHTML = "X"; | |
| closeButton.style.position = "absolute"; | |
| closeButton.style.top = "5px"; | |
| closeButton.style.right = "5px"; | |
| closeButton.style.border = "none"; | |
| closeButton.style.background = "none"; | |
| closeButton.style.cursor = "pointer"; | |
| closeButton.onclick = () => { | |
| document.body.removeChild(this.div); | |
| }; | |
| this.div.appendChild(title); | |
| this.div.appendChild(closeButton); | |
| } | |
| createFileInput() { | |
| // Create the file input and handle file selection | |
| this.fileInput = document.createElement("input"); | |
| this.fileInput.type = "file"; | |
| this.fileInput.accept = ".json"; | |
| this.fileInput.style.display = "none"; | |
| } | |
| createButton() { | |
| // Create the button | |
| this.button = document.createElement("button"); | |
| this.button.textContent = "Select File"; | |
| this.button.onclick = () => { | |
| this.fileInput.click(); | |
| }; | |
| } | |
| show() { | |
| return new Promise((resolve, reject) => { | |
| this.createDiv(); | |
| this.createFileInput(); | |
| this.createButton(); | |
| // Append elements to the div and the div to the body | |
| this.div.appendChild(this.button); | |
| this.div.appendChild(this.fileInput); | |
| document.body.appendChild(this.div); | |
| this.fileInput.onchange = async () => { | |
| // If no file is selected (user cancelled) | |
| if (!this.fileInput.files || this.fileInput.files.length === 0) { | |
| document.body.removeChild(this.div); | |
| alert("Operation canceled"); | |
| reject("File selection cancelled by user"); | |
| return; | |
| } | |
| try { | |
| // Process the file and resolve the promise | |
| const result = await this.fileProcessor(this.fileInput.files[0]); | |
| document.body.removeChild(this.div); | |
| resolve(result); | |
| } catch (error) { | |
| // Handle any errors in processing | |
| document.body.removeChild(this.div); | |
| reject(error); | |
| } | |
| }; | |
| }); | |
| } | |
| } | |
| class FileHelper { | |
| async readFileAsJsonAsync(file) { | |
| try { | |
| const fileContent = await this.readFileAsync(file); | |
| return JSON.parse(fileContent); | |
| } catch (error) { | |
| console.error("Error:", error); | |
| throw error; | |
| } | |
| } | |
| readFileAsync(file) { | |
| return new Promise((resolve, reject) => { | |
| let reader = new FileReader(); | |
| reader.onload = () => { | |
| resolve(reader.result); | |
| }; | |
| reader.onerror = () => { | |
| reject("Error reading file"); | |
| }; | |
| reader.readAsText(file); // Read the file as text | |
| }); | |
| } | |
| } | |
| // Helper function to convert currency values in rules | |
| function convertCurrencyInRule(rule, conversionRate, accountCurrency) { | |
| // Skip conversion if rate is 1 (already USD) | |
| console.log("Conversion rate: ", conversionRate); | |
| console.log("Account currency: ", accountCurrency); | |
| if (conversionRate === 1) { | |
| return rule; | |
| } | |
| // Deep clone the rule to avoid modifying the original | |
| const convertedRule = JSON.parse(JSON.stringify(rule)); | |
| // Get currency offsets | |
| const accountOffset = CURRENCY_OFFSETS[accountCurrency] || 100; | |
| const usdOffset = CURRENCY_OFFSETS["USD"] || 100; | |
| console.log("Account offset: ", accountOffset, "USD offset: ", usdOffset); | |
| // Convert currency values in evaluation_spec filters | |
| if (convertedRule.evaluation_spec && convertedRule.evaluation_spec.filters) { | |
| convertedRule.evaluation_spec.filters.forEach(filter => { | |
| // Check if this filter has a numeric value that might be currency | |
| if (filter.value && !isNaN(filter.value) && CURRENCY_FIELDS.includes(filter.field)) { | |
| // Convert the value to USD and round to 2 decimal places | |
| const originalValue = parseFloat(filter.value); | |
| // First convert to base value, then to USD, then adjust for USD offset | |
| const usdValue = (originalValue / conversionRate) * (usdOffset / accountOffset); | |
| console.log("Original value: ", originalValue, "USD value: ", usdValue) | |
| filter.value = Math.round(usdValue).toString(); | |
| } | |
| }); | |
| } | |
| return convertedRule; | |
| } | |
| // Helper function to convert currency values in rules back to original currency | |
| function convertCurrencyFromUSD(rule, conversionRate, accountCurrency) { | |
| // Skip conversion if rate is 1 (already USD) | |
| if (conversionRate === 1) { | |
| return rule; | |
| } | |
| // Deep clone the rule to avoid modifying the original | |
| const convertedRule = JSON.parse(JSON.stringify(rule)); | |
| // Get currency offsets | |
| const accountOffset = CURRENCY_OFFSETS[accountCurrency] || 100; | |
| const usdOffset = CURRENCY_OFFSETS["USD"] || 100; | |
| // Convert currency values in evaluation_spec filters | |
| if (convertedRule.evaluation_spec && convertedRule.evaluation_spec.filters) { | |
| convertedRule.evaluation_spec.filters.forEach(filter => { | |
| // Check if this filter has a numeric value that might be currency | |
| if (filter.value && !isNaN(filter.value) && CURRENCY_FIELDS.includes(filter.field)) { | |
| // Convert the value from USD to account currency and round to 2 decimal places | |
| const usdValue = parseFloat(filter.value); | |
| const accountValue = usdValue / usdOffset * conversionRate * accountOffset; | |
| console.log("USD value: ", usdValue, "Account value: ", accountValue) | |
| filter.value = Math.round(accountValue).toString(); | |
| } | |
| }); | |
| } | |
| return convertedRule; | |
| } | |
| // Main functions for autorules export/import | |
| async function exportAutorules(accountId = null) { | |
| const api = new FbApi(); | |
| const rulesApi = new FbRules(); | |
| // If no accountId provided, try to get from current context | |
| if (!accountId) { | |
| accountId = require("BusinessUnifiedNavigationContext").adAccountID; | |
| } | |
| const errorLog = []; | |
| try { | |
| // Get account info from cached data | |
| logMessage(`Getting account info for ${accountId}...`); | |
| const accountData = allAccountsData.find(acc => acc.id === accountId); | |
| const conversionRate = accountData?.conversionRate || 1; | |
| const currency = accountData?.currency || "USD"; | |
| logMessage(`Account currency: ${currency}, conversion rate: ${conversionRate}`); | |
| // Get all rules for the account | |
| logMessage("Getting autorules..."); | |
| const rulesResponse = await rulesApi.getAllRules(accountId); | |
| if (!rulesResponse.data || rulesResponse.data.length === 0) { | |
| const message = "No autorules found for this account."; | |
| logMessage(message, "warning"); | |
| return; | |
| } | |
| // Convert all currency values to USD | |
| const rulesInUSD = rulesResponse.data.map(rule => { | |
| try { | |
| // Parse JSON strings in rule | |
| if (typeof rule.evaluation_spec === 'string') { | |
| rule.evaluation_spec = JSON.parse(rule.evaluation_spec); | |
| } | |
| if (typeof rule.execution_spec === 'string') { | |
| rule.execution_spec = JSON.parse(rule.execution_spec); | |
| } | |
| if (typeof rule.schedule_spec === 'string') { | |
| rule.schedule_spec = JSON.parse(rule.schedule_spec); | |
| } | |
| // Extract only the required fields | |
| const extractedRule = { | |
| id: rule.id, | |
| name: rule.name, | |
| evaluation_spec: rule.evaluation_spec, | |
| execution_spec: rule.execution_spec, | |
| schedule_spec: rule.schedule_spec, | |
| status: rule.status | |
| }; | |
| // Convert currency values | |
| return convertCurrencyInRule(extractedRule, conversionRate, currency); | |
| } catch (ruleError) { | |
| const errorMessage = `Error processing rule ${rule.name || rule.id}: ${ruleError.message || ruleError}`; | |
| console.error(errorMessage); | |
| errorLog.push(errorMessage); | |
| return null; | |
| } | |
| }).filter(rule => rule !== null); | |
| // Prepare export data | |
| const exportData = { | |
| rules: rulesInUSD, | |
| metadata: { | |
| exportDate: new Date().toISOString(), | |
| sourceAccountId: accountId, | |
| sourceCurrency: currency, | |
| conversionRate: conversionRate | |
| } | |
| }; | |
| // Create file for download | |
| const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: "application/json" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = `autorules_${accountId}_${new Date().toISOString().split('T')[0]}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| const successMessage = `Successfully exported ${rulesInUSD.length} autorules (converted to USD).`; | |
| logMessage(successMessage, "success"); | |
| if (errorLog.length > 0) { | |
| errorLog.forEach(err => logMessage(err, "error")); | |
| } | |
| } catch (error) { | |
| const errorMessage = `Error exporting autorules: ${error.message || error}`; | |
| logMessage(errorMessage, "error"); | |
| } | |
| } | |
| // Helper function to import rules to a single account | |
| async function importRulesToAccount(accountId, rules, clearExisting, mainErrorLog = []) { | |
| const rulesApi = new FbRules(); | |
| const accountErrorLog = []; | |
| let importedCount = 0; | |
| try { | |
| // Get account info from cached data | |
| logMessage(`Getting info for account ${accountId}...`); | |
| const accountData = allAccountsData.find(acc => acc.id === accountId); | |
| const conversionRate = accountData?.conversionRate || 1; | |
| const currency = accountData?.currency || "USD"; | |
| const accountName = accountData?.name || accountId; | |
| logMessage(`Account: ${accountName}, currency: ${currency}`); | |
| // Clear existing rules if requested | |
| if (clearExisting) { | |
| console.log(`Checking for existing rules in account ${accountId}...`); | |
| const existingRules = await rulesApi.getAllRules(accountId); | |
| if (existingRules.data && existingRules.data.length > 0) { | |
| await rulesApi.clearRules(accountId); | |
| const clearMessage = `Cleared ${existingRules.data.length} existing autorules from account ${accountName} (${accountId}).`; | |
| console.log(clearMessage); | |
| accountErrorLog.push(clearMessage); | |
| if (mainErrorLog) mainErrorLog.push(clearMessage); | |
| } | |
| } | |
| // Import rules with currency conversion | |
| logMessage(`Importing ${rules.length} autorules to account ${accountId}...`); | |
| // Convert rules from USD to account currency | |
| const convertedRules = rules.map(rule => convertCurrencyFromUSD(rule, conversionRate, currency)); | |
| for (let i = 0; i < convertedRules.length; i++) { | |
| const convertedRule = convertedRules[i]; | |
| try { | |
| // Verify rule has all required fields | |
| if (!convertedRule.name || !convertedRule.evaluation_spec || !convertedRule.execution_spec || !convertedRule.schedule_spec) { | |
| const errorMessage = `Skipping rule ${convertedRule.name || 'unknown'} for account ${accountName} (${accountId}): Missing required fields`; | |
| console.error(errorMessage); | |
| accountErrorLog.push(errorMessage); | |
| if (mainErrorLog) mainErrorLog.push(errorMessage); | |
| continue; | |
| } | |
| // Remove ID if present (to create a new rule) | |
| delete convertedRule.id; | |
| // Prepare rule with only required fields and properly stringify specs | |
| const name = convertedRule.name; | |
| const evalSpec = convertedRule.evaluation_spec; | |
| const execSpec = convertedRule.execution_spec; | |
| const schedSpec = convertedRule.schedule_spec; | |
| const status = convertedRule.status || 'ACTIVE'; | |
| // Add rule | |
| const response = await rulesApi.addRule(accountId, name, evalSpec, execSpec, schedSpec); | |
| // Check for errors in the response | |
| if (response.error) { | |
| const errorMessage = `Error adding rule ${name} to account ${accountName} (${accountId}): ${response.error.message || JSON.stringify(response.error)}`; | |
| console.error(errorMessage); | |
| accountErrorLog.push(errorMessage); | |
| if (mainErrorLog) mainErrorLog.push(errorMessage); | |
| continue; | |
| } | |
| importedCount++; | |
| // Add delay between rule creation to avoid rate limiting (500ms) | |
| if (i < convertedRules.length - 1) { | |
| logMessage(`Created rule ${i+1}/${convertedRules.length}, waiting 500ms...`); | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| } | |
| } catch (ruleError) { | |
| const errorMessage = `Error importing rule ${convertedRule.name || 'unknown'} to account ${accountName} (${accountId}): ${ruleError.message || ruleError}`; | |
| console.error(errorMessage); | |
| accountErrorLog.push(errorMessage); | |
| if (mainErrorLog) mainErrorLog.push(errorMessage); | |
| } | |
| } | |
| const successMessage = `Successfully imported ${importedCount}/${rules.length} autorules to ${accountName}.`; | |
| logMessage(successMessage, "success"); | |
| accountErrorLog.unshift(successMessage); | |
| if (mainErrorLog) mainErrorLog.push(successMessage); | |
| return { success: true, importedCount, totalRules: rules.length, errorLog: accountErrorLog }; | |
| } catch (error) { | |
| const errorMessage = `Error processing account ${accountId}: ${error.message || error}`; | |
| console.error(errorMessage); | |
| accountErrorLog.push(errorMessage); | |
| if (mainErrorLog) mainErrorLog.push(errorMessage); | |
| return { success: false, importedCount, totalRules: rules.length, errorLog: accountErrorLog }; | |
| } | |
| } | |
| async function importAutorulesToSelectedAccounts(accountIds, uiInstance) { | |
| const fileHelper = new FileHelper(); | |
| const errorLog = []; | |
| try { | |
| // Let user select file | |
| const fileSelector = new FileSelector(file => fileHelper.readFileAsJsonAsync(file)); | |
| const fileContent = await fileSelector.show(); | |
| if (!fileContent) return; | |
| // Validate file content | |
| if (!fileContent.rules || !Array.isArray(fileContent.rules)) { | |
| const message = "Invalid file format. Expected a JSON file with 'rules' array."; | |
| logMessage(message, "error"); | |
| return; | |
| } | |
| // Check if we should clear existing rules (using the checkbox from the UI) | |
| const clearExisting = document.getElementById("ywbClearExistingRules").checked; | |
| logMessage(`Clear existing rules: ${clearExisting}`); | |
| // Process each account | |
| logMessage(`Processing ${accountIds.length} accounts...`); | |
| let successCount = 0; | |
| let failedCount = 0; | |
| for (let i = 0; i < accountIds.length; i++) { | |
| const accountId = accountIds[i]; | |
| logMessage(`Processing account ${accountId} (${i+1}/${accountIds.length})...`); | |
| // Import rules to this account | |
| const result = await importRulesToAccount(accountId, fileContent.rules, clearExisting, errorLog); | |
| if (result.success) { | |
| successCount++; | |
| // Update rule count | |
| if (clearExisting) { | |
| updateAccountRuleCount(accountId, result.importedCount); | |
| } else { | |
| addToAccountRuleCount(accountId, result.importedCount); | |
| } | |
| } else { | |
| failedCount++; | |
| } | |
| // Add delay between accounts to avoid rate limiting (1000ms) | |
| if (i < accountIds.length - 1) { | |
| logMessage(`Waiting 1 second before processing next account...`); | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |
| } | |
| // Refresh dropdowns with updated counts | |
| if (uiInstance) { | |
| uiInstance.refreshDropdowns(); | |
| } | |
| const summaryMessage = `Processed ${accountIds.length} accounts: ${successCount} successful, ${failedCount} failed.`; | |
| logMessage(summaryMessage, "success"); | |
| } catch (error) { | |
| const errorMessage = `Error importing autorules: ${error.message || error}`; | |
| logMessage(errorMessage, "error"); | |
| } | |
| } | |
| // Delete rules from selected accounts | |
| async function deleteRulesFromSelectedAccounts(accountIds, uiInstance) { | |
| const rulesApi = new FbRules(); | |
| const errorLog = []; | |
| try { | |
| logMessage(`Deleting rules from ${accountIds.length} accounts...`); | |
| let successCount = 0; | |
| let failedCount = 0; | |
| for (let i = 0; i < accountIds.length; i++) { | |
| const accountId = accountIds[i]; | |
| logMessage(`Deleting from account ${accountId} (${i+1}/${accountIds.length})...`); | |
| try { | |
| await rulesApi.clearRules(accountId); | |
| successCount++; | |
| // Update rule count to 0 | |
| updateAccountRuleCount(accountId, 0); | |
| logMessage(`✓ Deleted rules from account ${accountId}`, "success"); | |
| errorLog.push(`Successfully deleted rules from account ${accountId}`); | |
| } catch (error) { | |
| failedCount++; | |
| const errorMessage = `Error deleting rules from account ${accountId}: ${error.message || error}`; | |
| console.error(errorMessage); | |
| errorLog.push(errorMessage); | |
| } | |
| // Add delay between accounts to avoid rate limiting (1000ms) | |
| if (i < accountIds.length - 1) { | |
| logMessage(`Waiting 1 second before processing next account...`); | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |
| } | |
| // Refresh dropdowns with updated counts | |
| if (uiInstance) { | |
| uiInstance.refreshDropdowns(); | |
| } | |
| const summaryMessage = `Processed ${accountIds.length} accounts: ${successCount} successful, ${failedCount} failed.`; | |
| logMessage(summaryMessage, "success"); | |
| } catch (error) { | |
| const errorMessage = `Error deleting rules: ${error.message || error}`; | |
| logMessage(errorMessage, "error"); | |
| } | |
| } | |
| // Load all accounts with their rule counts | |
| async function loadAllAccountsWithRules() { | |
| const api = new FbApi(); | |
| const rulesApi = new FbRules(); | |
| try { | |
| logMessage("Loading all accounts with rule counts..."); | |
| // Get all accounts with autorules count and currency info in a single request using field expansion | |
| const accounts = await api.getAllPages("me/adaccounts", "fields=id,name,account_status,currency,account_currency_ratio_to_usd,adrules_library.limit(100){id,name}"); | |
| // Map accounts with rule counts from the data array | |
| const accountsWithRules = accounts.map(account => { | |
| const accountId = account.id.replace("act_", ""); | |
| const ruleCount = account.adrules_library?.data?.length || 0; | |
| return { | |
| id: accountId, | |
| name: account.name || accountId, | |
| ruleCount: ruleCount, | |
| status: account.account_status, | |
| currency: account.currency || "USD", | |
| conversionRate: account.account_currency_ratio_to_usd || 1 | |
| }; | |
| }); | |
| allAccountsData = accountsWithRules; | |
| logMessage(`Loaded ${allAccountsData.length} accounts.`, "success"); | |
| return allAccountsData; | |
| } catch (error) { | |
| console.error("Error loading accounts:", error); | |
| throw error; | |
| } | |
| } | |
| // Update rule count for a specific account | |
| function updateAccountRuleCount(accountId, newCount) { | |
| const account = allAccountsData.find(acc => acc.id === accountId); | |
| if (account) { | |
| account.ruleCount = newCount; | |
| } | |
| } | |
| // Add to rule count for a specific account | |
| function addToAccountRuleCount(accountId, countToAdd) { | |
| const account = allAccountsData.find(acc => acc.id === accountId); | |
| if (account) { | |
| account.ruleCount += countToAdd; | |
| } | |
| } | |
| // Create UI for autorules manager | |
| class AutorulesManagerUI { | |
| constructor() { | |
| this.div = null; | |
| this.buttons = {}; | |
| this.selectedExportAccountId = null; | |
| this.selectedImportAccountIds = []; | |
| this.logArea = null; | |
| } | |
| createDiv() { | |
| this.div = document.createElement("div"); | |
| this.div.style.position = "fixed"; | |
| this.div.style.top = "50%"; | |
| this.div.style.left = "50%"; | |
| this.div.style.transform = "translate(-50%, -50%)"; | |
| this.div.style.width = "400px"; | |
| this.div.style.maxHeight = "90vh"; | |
| this.div.style.overflowY = "auto"; | |
| this.div.style.backgroundColor = "yellow"; | |
| this.div.style.zIndex = "1000"; | |
| this.div.style.display = "flex"; | |
| this.div.style.flexDirection = "column"; | |
| this.div.style.alignItems = "center"; | |
| this.div.style.justifyContent = "flex-start"; | |
| this.div.style.padding = "20px"; | |
| this.div.style.boxSizing = "border-box"; | |
| this.div.style.borderRadius = "10px"; | |
| this.div.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)"; | |
| // Create and style the title | |
| const title = document.createElement("div"); | |
| title.innerHTML = "<h2>FB Autorules Manager "+currentVersion+"</h2><p><a href='https://yellowweb.top' target='_blank'>by Yellow Web</a></p>"; | |
| title.style.textAlign = "center"; | |
| title.style.marginBottom = "20px"; | |
| // Create and style the close button | |
| const closeButton = document.createElement("button"); | |
| closeButton.innerHTML = "X"; | |
| closeButton.style.position = "absolute"; | |
| closeButton.style.top = "10px"; | |
| closeButton.style.right = "10px"; | |
| closeButton.style.border = "none"; | |
| closeButton.style.background = "none"; | |
| closeButton.style.fontSize = "18px"; | |
| closeButton.style.cursor = "pointer"; | |
| closeButton.onclick = () => { | |
| document.body.removeChild(this.div); | |
| }; | |
| // We've moved the copy as bookmark functionality to a link at the bottom | |
| this.div.appendChild(title); | |
| this.div.appendChild(closeButton); | |
| return this.div; | |
| } | |
| createButton(id, text, onClick) { | |
| const button = document.createElement("button"); | |
| button.id = id; | |
| button.textContent = text; | |
| button.style.margin = "10px 0"; | |
| button.style.padding = "10px 15px"; | |
| button.style.width = "100%"; | |
| button.style.backgroundColor = "#4CAF50"; | |
| button.style.color = "white"; | |
| button.style.border = "none"; | |
| button.style.borderRadius = "5px"; | |
| button.style.cursor = "pointer"; | |
| button.style.fontSize = "16px"; | |
| button.setAttribute("data-original-text", text); | |
| // Store the button in the buttons object | |
| this.buttons[id] = button; | |
| // Create a wrapper for the onClick function that handles button state | |
| button.onclick = async () => { | |
| this.setButtonLoading(id, true); | |
| try { | |
| await onClick(); | |
| } finally { | |
| this.setButtonLoading(id, false); | |
| } | |
| }; | |
| return button; | |
| } | |
| // Method to set button to loading state | |
| setButtonLoading(id, isLoading) { | |
| const button = this.buttons[id]; | |
| if (!button) return; | |
| if (isLoading) { | |
| button.disabled = true; | |
| button.style.opacity = "0.7"; | |
| button.style.cursor = "not-allowed"; | |
| button.textContent = "Working on it..."; | |
| } else { | |
| button.disabled = false; | |
| button.style.opacity = "1"; | |
| button.style.cursor = "pointer"; | |
| button.textContent = button.getAttribute("data-original-text"); | |
| } | |
| } | |
| // Create single-select dropdown for export | |
| createExportAccountDropdown() { | |
| const container = document.createElement("div"); | |
| container.style.width = "100%"; | |
| container.style.margin = "10px 0"; | |
| const label = document.createElement("label"); | |
| label.textContent = "Select account to export from:"; | |
| label.style.display = "block"; | |
| label.style.marginBottom = "5px"; | |
| label.style.fontSize = "14px"; | |
| label.style.fontWeight = "bold"; | |
| const select = document.createElement("select"); | |
| select.id = "ywbExportAccountSelect"; | |
| select.style.width = "100%"; | |
| select.style.padding = "8px"; | |
| select.style.borderRadius = "5px"; | |
| select.style.border = "1px solid #ccc"; | |
| select.style.fontSize = "14px"; | |
| // Add default option | |
| const defaultOption = document.createElement("option"); | |
| defaultOption.value = ""; | |
| defaultOption.textContent = "-- Choose an account --"; | |
| defaultOption.disabled = true; | |
| defaultOption.selected = true; | |
| select.appendChild(defaultOption); | |
| // Add account options | |
| allAccountsData.forEach(account => { | |
| const option = document.createElement("option"); | |
| option.value = account.id; | |
| option.textContent = `${account.id} - ${account.name} [${account.ruleCount} rules]`; | |
| select.appendChild(option); | |
| }); | |
| // Store selected account | |
| select.onchange = () => { | |
| this.selectedExportAccountId = select.value; | |
| }; | |
| container.appendChild(label); | |
| container.appendChild(select); | |
| return container; | |
| } | |
| // Create multi-select dropdown for import | |
| createImportAccountDropdown() { | |
| const container = document.createElement("div"); | |
| container.style.width = "100%"; | |
| container.style.margin = "10px 0"; | |
| const label = document.createElement("label"); | |
| label.textContent = "Select accounts to import to:"; | |
| label.style.display = "block"; | |
| label.style.marginBottom = "5px"; | |
| label.style.fontSize = "14px"; | |
| label.style.fontWeight = "bold"; | |
| const selectAllContainer = document.createElement("div"); | |
| selectAllContainer.style.marginBottom = "5px"; | |
| const selectAllCheckbox = document.createElement("input"); | |
| selectAllCheckbox.type = "checkbox"; | |
| selectAllCheckbox.id = "ywbSelectAllAccounts"; | |
| selectAllCheckbox.style.marginRight = "5px"; | |
| const selectAllLabel = document.createElement("label"); | |
| selectAllLabel.htmlFor = "ywbSelectAllAccounts"; | |
| selectAllLabel.textContent = "Select All Accounts"; | |
| selectAllLabel.style.fontSize = "12px"; | |
| selectAllLabel.style.fontStyle = "italic"; | |
| selectAllContainer.appendChild(selectAllCheckbox); | |
| selectAllContainer.appendChild(selectAllLabel); | |
| const select = document.createElement("select"); | |
| select.id = "ywbImportAccountSelect"; | |
| select.multiple = true; | |
| select.size = Math.min(allAccountsData.length, 8); | |
| select.style.width = "100%"; | |
| select.style.padding = "5px"; | |
| select.style.borderRadius = "5px"; | |
| select.style.border = "1px solid #ccc"; | |
| select.style.fontSize = "12px"; | |
| // Add account options | |
| allAccountsData.forEach(account => { | |
| const option = document.createElement("option"); | |
| option.value = account.id; | |
| option.textContent = `${account.id} - ${account.name} [${account.ruleCount} rules]`; | |
| select.appendChild(option); | |
| }); | |
| // Store selected accounts | |
| const updateSelection = () => { | |
| this.selectedImportAccountIds = Array.from(select.selectedOptions).map(opt => opt.value); | |
| }; | |
| select.onchange = updateSelection; | |
| // Select all checkbox functionality | |
| selectAllCheckbox.onchange = () => { | |
| if (selectAllCheckbox.checked) { | |
| Array.from(select.options).forEach(opt => opt.selected = true); | |
| } else { | |
| Array.from(select.options).forEach(opt => opt.selected = false); | |
| } | |
| updateSelection(); | |
| }; | |
| container.appendChild(label); | |
| container.appendChild(selectAllContainer); | |
| container.appendChild(select); | |
| return container; | |
| } | |
| // Refresh dropdown options with updated rule counts | |
| refreshDropdowns() { | |
| const exportSelect = document.getElementById("ywbExportAccountSelect"); | |
| const importSelect = document.getElementById("ywbImportAccountSelect"); | |
| if (exportSelect) { | |
| // Store current selection | |
| const currentValue = exportSelect.value; | |
| // Clear and rebuild options | |
| exportSelect.innerHTML = ""; | |
| const defaultOption = document.createElement("option"); | |
| defaultOption.value = ""; | |
| defaultOption.textContent = "-- Choose an account --"; | |
| defaultOption.disabled = true; | |
| defaultOption.selected = !currentValue; | |
| exportSelect.appendChild(defaultOption); | |
| allAccountsData.forEach(account => { | |
| const option = document.createElement("option"); | |
| option.value = account.id; | |
| option.textContent = `${account.id} - ${account.name} [${account.ruleCount} rules]`; | |
| if (account.id === currentValue) { | |
| option.selected = true; | |
| } | |
| exportSelect.appendChild(option); | |
| }); | |
| } | |
| if (importSelect) { | |
| // Store current selection | |
| const currentValues = Array.from(importSelect.selectedOptions).map(opt => opt.value); | |
| // Clear and rebuild options | |
| importSelect.innerHTML = ""; | |
| allAccountsData.forEach(account => { | |
| const option = document.createElement("option"); | |
| option.value = account.id; | |
| option.textContent = `${account.id} - ${account.name} [${account.ruleCount} rules]`; | |
| if (currentValues.includes(account.id)) { | |
| option.selected = true; | |
| } | |
| importSelect.appendChild(option); | |
| }); | |
| } | |
| } | |
| createTabs() { | |
| // Tab container | |
| const tabContainer = document.createElement("div"); | |
| tabContainer.style.display = "flex"; | |
| tabContainer.style.width = "100%"; | |
| tabContainer.style.marginBottom = "15px"; | |
| tabContainer.style.borderBottom = "2px solid #333"; | |
| // Export/Delete tab | |
| const exportTab = document.createElement("button"); | |
| exportTab.id = "ywbExportTab"; | |
| exportTab.textContent = "Export / Delete"; | |
| exportTab.style.flex = "1"; | |
| exportTab.style.padding = "10px"; | |
| exportTab.style.border = "none"; | |
| exportTab.style.background = "none"; | |
| exportTab.style.cursor = "pointer"; | |
| exportTab.style.fontSize = "14px"; | |
| exportTab.style.fontWeight = "bold"; | |
| exportTab.style.borderBottom = "3px solid #333"; | |
| // Import tab | |
| const importTab = document.createElement("button"); | |
| importTab.id = "ywbImportTab"; | |
| importTab.textContent = "Import"; | |
| importTab.style.flex = "1"; | |
| importTab.style.padding = "10px"; | |
| importTab.style.border = "none"; | |
| importTab.style.background = "none"; | |
| importTab.style.cursor = "pointer"; | |
| importTab.style.fontSize = "14px"; | |
| importTab.style.fontWeight = "bold"; | |
| // Tab click handlers | |
| exportTab.onclick = () => { | |
| exportTab.style.borderBottom = "3px solid #333"; | |
| importTab.style.borderBottom = "none"; | |
| document.getElementById("ywbExportTabContent").style.display = "block"; | |
| document.getElementById("ywbImportTabContent").style.display = "none"; | |
| }; | |
| importTab.onclick = () => { | |
| importTab.style.borderBottom = "3px solid #333"; | |
| exportTab.style.borderBottom = "none"; | |
| document.getElementById("ywbExportTabContent").style.display = "none"; | |
| document.getElementById("ywbImportTabContent").style.display = "block"; | |
| }; | |
| tabContainer.appendChild(exportTab); | |
| tabContainer.appendChild(importTab); | |
| return tabContainer; | |
| } | |
| // Create log area | |
| createLogArea() { | |
| const logContainer = document.createElement("div"); | |
| logContainer.style.width = "100%"; | |
| logContainer.style.marginTop = "15px"; | |
| logContainer.style.borderTop = "2px solid #333"; | |
| logContainer.style.paddingTop = "10px"; | |
| const logLabel = document.createElement("div"); | |
| logLabel.textContent = "Log:"; | |
| logLabel.style.fontSize = "12px"; | |
| logLabel.style.fontWeight = "bold"; | |
| logLabel.style.marginBottom = "5px"; | |
| this.logArea = document.createElement("div"); | |
| this.logArea.id = "ywbLogArea"; | |
| this.logArea.style.width = "100%"; | |
| this.logArea.style.height = "120px"; | |
| this.logArea.style.overflowY = "auto"; | |
| this.logArea.style.backgroundColor = "#f5f5f5"; | |
| this.logArea.style.border = "1px solid #ccc"; | |
| this.logArea.style.borderRadius = "5px"; | |
| this.logArea.style.padding = "8px"; | |
| this.logArea.style.fontSize = "11px"; | |
| this.logArea.style.fontFamily = "monospace"; | |
| this.logArea.style.lineHeight = "1.4"; | |
| logContainer.appendChild(logLabel); | |
| logContainer.appendChild(this.logArea); | |
| return logContainer; | |
| } | |
| // Add message to log | |
| log(message, type = "info") { | |
| if (!this.logArea) return; | |
| const logEntry = document.createElement("div"); | |
| logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; | |
| if (type === "error") { | |
| logEntry.style.color = "red"; | |
| } else if (type === "success") { | |
| logEntry.style.color = "green"; | |
| } else if (type === "warning") { | |
| logEntry.style.color = "orange"; | |
| } | |
| this.logArea.appendChild(logEntry); | |
| // Auto-scroll to bottom | |
| this.logArea.scrollTop = this.logArea.scrollHeight; | |
| } | |
| // Clear log | |
| clearLog() { | |
| if (this.logArea) { | |
| this.logArea.innerHTML = ""; | |
| } | |
| } | |
| show() { | |
| const div = this.createDiv(); | |
| // Create tabs | |
| const tabs = this.createTabs(); | |
| div.appendChild(tabs); | |
| // Export/Delete Tab Content | |
| const exportTabContent = document.createElement("div"); | |
| exportTabContent.id = "ywbExportTabContent"; | |
| exportTabContent.style.width = "100%"; | |
| exportTabContent.style.display = "block"; | |
| const exportDropdown = this.createExportAccountDropdown(); | |
| const exportButton = this.createButton("export-btn", "Export Autorules to JSON", async () => { | |
| if (!this.selectedExportAccountId) { | |
| alert("Please select an account to export from."); | |
| return; | |
| } | |
| await exportAutorules(this.selectedExportAccountId); | |
| }); | |
| const deleteButton = this.createButton("delete-export-btn", "Delete Rules from Selected Account", async () => { | |
| if (!this.selectedExportAccountId) { | |
| alert("Please select an account to delete rules from."); | |
| return; | |
| } | |
| const confirmMsg = `Are you sure you want to delete all rules from the selected account?`; | |
| if (!confirm(confirmMsg)) { | |
| return; | |
| } | |
| await deleteRulesFromSelectedAccounts([this.selectedExportAccountId], this); | |
| }); | |
| exportTabContent.appendChild(exportDropdown); | |
| exportTabContent.appendChild(exportButton); | |
| exportTabContent.appendChild(deleteButton); | |
| // Import Tab Content | |
| const importTabContent = document.createElement("div"); | |
| importTabContent.id = "ywbImportTabContent"; | |
| importTabContent.style.width = "100%"; | |
| importTabContent.style.display = "none"; | |
| const importDropdown = this.createImportAccountDropdown(); | |
| const checkboxContainer = document.createElement("div"); | |
| checkboxContainer.style.display = "flex"; | |
| checkboxContainer.style.alignItems = "center"; | |
| checkboxContainer.style.margin = "10px 0"; | |
| checkboxContainer.style.width = "100%"; | |
| const checkbox = document.createElement("input"); | |
| checkbox.type = "checkbox"; | |
| checkbox.id = "ywbClearExistingRules"; | |
| checkbox.style.marginRight = "10px"; | |
| const label = document.createElement("label"); | |
| label.htmlFor = "ywbClearExistingRules"; | |
| label.textContent = "Delete existing rules before import"; | |
| label.style.fontSize = "14px"; | |
| checkboxContainer.appendChild(checkbox); | |
| checkboxContainer.appendChild(label); | |
| const importButton = this.createButton("import-btn", "Import Autorules to Selected Accounts", async () => { | |
| if (!this.selectedImportAccountIds || this.selectedImportAccountIds.length === 0) { | |
| alert("Please select at least one account to import to."); | |
| return; | |
| } | |
| await importAutorulesToSelectedAccounts(this.selectedImportAccountIds, this); | |
| }); | |
| importTabContent.appendChild(importDropdown); | |
| importTabContent.appendChild(checkboxContainer); | |
| importTabContent.appendChild(importButton); | |
| // Add tab contents to div | |
| div.appendChild(exportTabContent); | |
| div.appendChild(importTabContent); | |
| // Add log area | |
| const logArea = this.createLogArea(); | |
| div.appendChild(logArea); | |
| // Create a small link for copying as bookmark | |
| const copyBookmarkLink = document.createElement("a"); | |
| copyBookmarkLink.href = "#"; | |
| copyBookmarkLink.textContent = "Copy as bookmark"; | |
| copyBookmarkLink.style.fontSize = "12px"; | |
| copyBookmarkLink.style.color = "blue"; | |
| copyBookmarkLink.style.textDecoration = "underline"; | |
| copyBookmarkLink.style.cursor = "pointer"; | |
| copyBookmarkLink.style.marginTop = "10px"; | |
| copyBookmarkLink.style.display = "block"; | |
| copyBookmarkLink.style.textAlign = "center"; | |
| copyBookmarkLink.onclick = (e) => { | |
| e.preventDefault(); | |
| copyScriptAsBase64Bookmarklet(); | |
| }; | |
| div.appendChild(copyBookmarkLink); | |
| // Add div to body | |
| document.body.appendChild(div); | |
| // Initial log message | |
| this.log("UI initialized. Ready to work.", "success"); | |
| } | |
| } | |
| // Main function to show the autorules manager UI | |
| async function showAutorulesManager() { | |
| try { | |
| // Show loading message | |
| const loadingDiv = document.createElement("div"); | |
| loadingDiv.style.position = "fixed"; | |
| loadingDiv.style.top = "50%"; | |
| loadingDiv.style.left = "50%"; | |
| loadingDiv.style.transform = "translate(-50%, -50%)"; | |
| loadingDiv.style.padding = "20px"; | |
| loadingDiv.style.backgroundColor = "yellow"; | |
| loadingDiv.style.borderRadius = "10px"; | |
| loadingDiv.style.zIndex = "1000"; | |
| loadingDiv.style.fontSize = "16px"; | |
| loadingDiv.style.fontWeight = "bold"; | |
| loadingDiv.textContent = "Loading accounts..."; | |
| document.body.appendChild(loadingDiv); | |
| // Load all accounts with rule counts | |
| await loadAllAccountsWithRules(); | |
| // Remove loading message | |
| document.body.removeChild(loadingDiv); | |
| // Show UI | |
| uiInstance = new AutorulesManagerUI(); | |
| uiInstance.show(); | |
| } catch (error) { | |
| console.error("Error loading accounts:", error); | |
| alert(`Error loading accounts: ${error.message || error}`); | |
| } | |
| } | |
| // Function to copy the script as base64 bookmarklet | |
| function copyScriptAsBase64Bookmarklet() { | |
| try { | |
| // Get the script URL - we'll use the current script's location | |
| const scriptUrl = window.location.href; | |
| // Create a string with all the code from this file | |
| // Since we can't easily get the source code in this context, we'll recreate it | |
| const scriptContent = `// Constants | |
| const currentVersion = "${currentVersion}"; | |
| let allAccountsData = []; | |
| let uiInstance = null; | |
| ${logMessage.toString()} | |
| const CURRENCY_FIELDS = ${JSON.stringify(CURRENCY_FIELDS)}; | |
| const CURRENCY_OFFSETS = ${JSON.stringify(CURRENCY_OFFSETS)}; | |
| ${FbApi.toString()} | |
| ${FbRules.toString()} | |
| ${FileSelector.toString()} | |
| ${FileHelper.toString()} | |
| ${convertCurrencyInRule.toString()} | |
| ${convertCurrencyFromUSD.toString()} | |
| ${exportAutorules.toString()} | |
| ${importRulesToAccount.toString()} | |
| ${importAutorulesToSelectedAccounts.toString()} | |
| ${deleteRulesFromSelectedAccounts.toString()} | |
| ${loadAllAccountsWithRules.toString()} | |
| ${updateAccountRuleCount.toString()} | |
| ${addToAccountRuleCount.toString()} | |
| ${AutorulesManagerUI.toString()} | |
| ${showAutorulesManager.toString()} | |
| ${copyScriptAsBase64Bookmarklet.toString()} | |
| // Make the functions available globally | |
| window.showAutorulesManager = showAutorulesManager; | |
| window.copyScriptAsBase64Bookmarklet = copyScriptAsBase64Bookmarklet; | |
| // Auto-run when script is loaded | |
| showAutorulesManager();`; | |
| // Encode the script content as base64 (UTF-8 safe) | |
| const base64Content = btoa(unescape(encodeURIComponent(scriptContent))); | |
| // Format the string as requested (decode UTF-8 properly) | |
| const bookmarkletCode = `javascript:eval("(async () => {" + decodeURIComponent(escape(atob("${base64Content}"))) + "})();");`; | |
| // Copy to clipboard | |
| navigator.clipboard.writeText(bookmarkletCode) | |
| .then(() => { | |
| alert("Bookmarklet copied to clipboard!"); | |
| }) | |
| .catch(err => { | |
| console.error('Failed to copy: ', err); | |
| // Fallback for browsers that don't support clipboard API | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = bookmarkletCode; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(textArea); | |
| alert("Bookmarklet copied to clipboard!"); | |
| }); | |
| } catch (error) { | |
| console.error('Error creating bookmarklet:', error); | |
| alert(`Error creating bookmarklet: ${error.message}`); | |
| } | |
| } | |
| // Make the functions available globally | |
| window.showAutorulesManager = showAutorulesManager; | |
| window.copyScriptAsBase64Bookmarklet = copyScriptAsBase64Bookmarklet; | |
| // Auto-run when script is loaded | |
| showAutorulesManager(); |
Author
Привет! Сегодня запускал скрипт и перестал рабоать
Напиши в тг @ywbfeedbackbot, посмотрю
Author
Привет! Сегодня запускал скрипт и перестал рабоать
Поправлено и обновлено
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Привет! Сегодня запускал скрипт и перестал рабоать
