Last active
March 14, 2025 20:20
-
-
Save saas-coder/22801e738de5695041270692959c23da to your computer and use it in GitHub Desktop.
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
| // Function to check if an ad has EU data without fetching the full data | |
| async function checkEUDataEligibility(requestIndex, adID) { | |
| // Check if we already have information about this ad | |
| const collectionIndex = findCollectionIndex(adsCollections, adID); | |
| if (collectionIndex === -1) return false; | |
| const adIndex = findAdIndex(adsCollections[collectionIndex], adID); | |
| if (adIndex === -1) return false; | |
| const ad = adsCollections[collectionIndex][adIndex]; | |
| // If we already know the eligibility status | |
| if (ad.is_aaa_eligible === false) { | |
| return "no_eu_data"; | |
| } | |
| if (ad.is_aaa_eligible === true) { | |
| return true; | |
| } | |
| // For non-political ads or ads not subject to transparency | |
| if (ad.is_not_political === true || ad.is_not_aaa_eligible === true) { | |
| ad.is_aaa_eligible = false; | |
| return "no_eu_data"; | |
| } | |
| // Check if there's a field in the ad data that indicates this directly | |
| if (ad.snapshot && ad.snapshot.ad_delivery_summary) { | |
| const deliverySummary = ad.snapshot.ad_delivery_summary; | |
| if (deliverySummary.hasOwnProperty('is_aaa_eligible')) { | |
| ad.is_aaa_eligible = deliverySummary.is_aaa_eligible; | |
| return ad.is_aaa_eligible ? true : "no_eu_data"; | |
| } | |
| } | |
| // Make a lightweight request to check | |
| try { | |
| const checkRequest = new AbortController(); | |
| const signal = checkRequest.signal; | |
| // Set a timeout to abort the request if it takes too long | |
| const timeoutId = setTimeout(() => checkRequest.abort(), 5000); | |
| // Construct request body (similar to fetchReachData but with minimal params) | |
| const minParams = { ...requestParams }; | |
| minParams.__req = reqID; | |
| minParams.__s = sessionValue; | |
| minParams.variables = JSON.stringify({ | |
| adArchiveID: adID, | |
| pageID: ad.pageID, | |
| country: "ALL", | |
| sessionID: sessionID, | |
| source: null, | |
| isAdNonPolitical: true, | |
| isAdNotAAAEligible: false | |
| }); | |
| minParams.doc_id = docID; | |
| const requestBody = toURLParams(minParams); | |
| // Make the request | |
| const response = await fetch(`${new URL(window.location).origin}/api/graphql/`, { | |
| method: "POST", | |
| headers: { | |
| "User-Agent": userAgent, | |
| "Accept": "*/*", | |
| "Accept-Language": "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| "X-FB-Friendly-Name": "AdLibraryAdDetailsV2Query", | |
| "X-FB-LSD": lsdToken, | |
| "X-ASBD-ID": asbdID, | |
| "Alt-Used": "www.facebook.com", | |
| "Sec-Fetch-Dest": "empty", | |
| "Sec-Fetch-Mode": "cors", | |
| "Sec-Fetch-Site": "same-origin", | |
| "Referer": referrerURL, | |
| "Origin": new URL(window.location).origin | |
| }, | |
| credentials: "same-origin", | |
| body: requestBody, | |
| signal | |
| }); | |
| clearTimeout(timeoutId); | |
| // Process response | |
| if (!response.ok) { | |
| return false; | |
| } | |
| const responseData = await response.json(); | |
| // Check if request is still relevant | |
| if (requestIndex !== lastRequestIndex) return false; | |
| if (responseData.errors || responseData.error) { | |
| return "auth_required"; | |
| } | |
| // Check if ad is eligible for AAA data | |
| if (!responseData.data || | |
| !responseData.data.ad_library_main || | |
| !responseData.data.ad_library_main.ad_details) { | |
| ad.is_aaa_eligible = false; | |
| return "no_eu_data"; | |
| } | |
| const hasAAAInfo = !!responseData.data.ad_library_main.ad_details.aaa_info; | |
| // Store the result | |
| ad.is_aaa_eligible = hasAAAInfo; | |
| return hasAAAInfo ? true : "no_eu_data"; | |
| } catch (error) { | |
| console.error("Error checking EU data eligibility:", error); | |
| return false; | |
| } | |
| } | |
| // Main data structure to store ads information | |
| let adsCollections = new Array(); | |
| let adCards = new Array(); | |
| // Variables to store session and request information | |
| let sessionID; | |
| let docID = "6635716889819821"; // GraphQL document ID for ad details query | |
| let requestParams; | |
| let reqID; | |
| let sessionValue; | |
| let lastRequestIndex = 0; | |
| let currentURL = window.location.href; | |
| let userAgent = window.navigator.userAgent; | |
| let referrerURL = window.location.href; | |
| let asbdID, lsdToken; | |
| let isFetching = false; | |
| // Process class names that identify ad cards | |
| const adCardClasses = ["xh8yej3", "xh8yej3 card-ad", "xh8yej3 oneuse card-ad"]; | |
| // Initialize listeners and monitor page changes | |
| function initializeExtension() { | |
| currentURL = window.location.href; | |
| userAgent = window.navigator.userAgent; | |
| referrerURL = window.location.href; | |
| } | |
| // Call initialization when DOM is loaded | |
| typeof window !== "undefined" && document.addEventListener("DOMContentLoaded", initializeExtension); | |
| // GraphQL request parameters class | |
| class RequestParameters { | |
| constructor(user, a, hs, dpr, ccg, rev, hsi, dyn, csr, dtsg, jazoest, lsd, aaid, spinR, spinB, spinT, jssesw) { | |
| this.av = user; | |
| this.__user = user; | |
| this.__a = a; | |
| this.__hs = hs; | |
| this.dpr = dpr; | |
| this.__ccg = ccg; | |
| this.__rev = rev; | |
| this.__hsi = hsi; | |
| this.__dyn = dyn; | |
| this.__csr = csr; | |
| this.fb_dtsg = dtsg; | |
| this.jazoest = jazoest; | |
| this.lsd = lsd; | |
| this.__aaid = aaid; | |
| this.__spin_r = spinR; | |
| this.__spin_b = spinB; | |
| this.__spin_t = spinT; | |
| this.__jssesw = jssesw; | |
| this.fb_api_caller_class = ["RelayModern"]; | |
| this.fb_api_req_friendly_name = ["AdLibraryAdDetailsV2Query"]; | |
| this.server_timestamps = ["true"]; | |
| } | |
| toJSON() { | |
| return { | |
| av: this.av, | |
| __user: this.__user, | |
| __a: this.__a, | |
| __hs: this.__hs, | |
| dpr: this.dpr, | |
| __ccg: this.__ccg, | |
| __rev: this.__rev, | |
| __hsi: this.__hsi, | |
| __dyn: this.__dyn, | |
| __csr: this.__csr, | |
| fb_dtsg: this.fb_dtsg, | |
| jazoest: this.jazoest, | |
| lsd: this.lsd, | |
| __aaid: this.__aaid, | |
| __spin_r: this.__spin_r, | |
| __spin_b: this.__spin_b, | |
| __spin_t: this.__spin_t, | |
| __jssesw: this.__jssesw, | |
| fb_api_caller_class: this.fb_api_caller_class, | |
| fb_api_req_friendly_name: this.fb_api_req_friendly_name, | |
| server_timestamps: this.server_timestamps | |
| }; | |
| } | |
| } | |
| // Format large numbers with K, M, etc. suffixes | |
| const formatNumber = (num, digits = 2) => { | |
| const units = [ | |
| { value: 1e18, symbol: "E" }, | |
| { value: 1e15, symbol: "P" }, | |
| { value: 1e12, symbol: "T" }, | |
| { value: 1e9, symbol: "G" }, | |
| { value: 1e6, symbol: "M" }, | |
| { value: 1e3, symbol: "K" }, | |
| { value: 1, symbol: "" } | |
| ]; | |
| const regex = /\.0+$|(\.[0-9]*[1-9])0+$/; | |
| const unit = units.find(unit => num >= unit.value) || units[units.length - 1]; | |
| return (num / unit.value).toFixed(digits).replace(regex, "$1") + unit.symbol; | |
| }; | |
| // Create reach display with button for an ad card | |
| function createReachButton(card, adID) { | |
| if (!card) return false; | |
| // Create display container | |
| const container = document.createElement("div"); | |
| container.classList.add("reach-display-container"); | |
| container.style.padding = "8px 16px"; | |
| container.style.marginBottom = "8px"; | |
| // Create reach info display | |
| const reachInfo = document.createElement("div"); | |
| reachInfo.classList.add("reach-info"); | |
| reachInfo.style.display = "flex"; | |
| reachInfo.style.justifyContent = "space-between"; | |
| reachInfo.style.alignItems = "center"; | |
| reachInfo.style.backgroundColor = "#F0F2F5"; | |
| reachInfo.style.borderRadius = "6px"; | |
| reachInfo.style.padding = "12px"; | |
| reachInfo.style.marginBottom = "8px"; | |
| // Left side - reach button/data | |
| const reachData = document.createElement("div"); | |
| reachData.classList.add("reach-data"); | |
| reachData.style.display = "flex"; | |
| reachData.style.flexDirection = "column"; | |
| const reachLabel = document.createElement("div"); | |
| reachLabel.style.fontSize = "13px"; | |
| reachLabel.style.color = "#65676B"; | |
| reachLabel.style.marginBottom = "4px"; | |
| reachLabel.textContent = "Estimated Ad Reach"; | |
| // Create fetch button | |
| const fetchButton = document.createElement("button"); | |
| fetchButton.classList.add("fetch-reach-button"); | |
| fetchButton.style.backgroundColor = "#1877F2"; | |
| fetchButton.style.color = "white"; | |
| fetchButton.style.border = "none"; | |
| fetchButton.style.borderRadius = "6px"; | |
| fetchButton.style.padding = "6px 12px"; | |
| fetchButton.style.fontSize = "14px"; | |
| fetchButton.style.fontWeight = "600"; | |
| fetchButton.style.cursor = "pointer"; | |
| fetchButton.style.transition = "background-color 0.2s"; | |
| fetchButton.textContent = "Get Reach Data"; | |
| // Create result display (initially hidden) | |
| const reachValue = document.createElement("div"); | |
| reachValue.style.fontSize = "16px"; | |
| reachValue.style.fontWeight = "600"; | |
| reachValue.style.color = "#1C1E21"; | |
| reachValue.style.display = "none"; | |
| reachValue.textContent = ""; | |
| reachData.appendChild(reachLabel); | |
| reachData.appendChild(fetchButton); | |
| reachData.appendChild(reachValue); | |
| // Right side - ad details link | |
| const adDetailsLink = document.createElement("a"); | |
| adDetailsLink.classList.add("ad-details-link"); | |
| adDetailsLink.style.textDecoration = "none"; | |
| adDetailsLink.style.backgroundColor = "#1877F2"; | |
| adDetailsLink.style.color = "white"; | |
| adDetailsLink.style.padding = "8px 12px"; | |
| adDetailsLink.style.borderRadius = "6px"; | |
| adDetailsLink.style.fontSize = "14px"; | |
| adDetailsLink.style.fontWeight = "600"; | |
| adDetailsLink.style.cursor = "pointer"; | |
| adDetailsLink.style.transition = "background-color 0.2s"; | |
| adDetailsLink.textContent = "See Ad Details"; | |
| // Compose the reach info display | |
| reachInfo.appendChild(reachData); | |
| reachInfo.appendChild(adDetailsLink); | |
| // Add hover effects | |
| fetchButton.addEventListener("mouseover", () => { | |
| fetchButton.style.backgroundColor = "#166FE5"; | |
| }); | |
| fetchButton.addEventListener("mouseout", () => { | |
| fetchButton.style.backgroundColor = "#1877F2"; | |
| }); | |
| adDetailsLink.addEventListener("mouseover", () => { | |
| adDetailsLink.style.backgroundColor = "#166FE5"; | |
| }); | |
| adDetailsLink.addEventListener("mouseout", () => { | |
| adDetailsLink.style.backgroundColor = "#1877F2"; | |
| }); | |
| // Add fetch button click handler | |
| fetchButton.addEventListener("click", async () => { | |
| // Show loading state | |
| fetchButton.disabled = true; | |
| fetchButton.textContent = "Loading..."; | |
| reachValue.style.display = "block"; | |
| reachValue.textContent = "Fetching reach data..."; | |
| try { | |
| // Get collation ID (ad group ID) | |
| const collectionIndex = findCollectionIndex(adsCollections, adID); | |
| if (collectionIndex === -1) { | |
| reachValue.textContent = "No reach data available"; | |
| fetchButton.style.display = "none"; | |
| return; | |
| } | |
| const ad = adsCollections[collectionIndex][0]; | |
| const isCollation = ad.collationCount && ad.collationCount > 1; | |
| if (isCollation) { | |
| // For ad groups, show it's a group | |
| reachValue.textContent = "Ad Group - Calculating Total Reach"; | |
| // Get all ads in this collation | |
| const allAdsInGroup = await fetchAllAdsInCollation( | |
| lastRequestIndex, | |
| ad.collationID, | |
| [], | |
| ad.collationCount | |
| ); | |
| if (!allAdsInGroup) { | |
| reachValue.textContent = "No reach data available for this ad group"; | |
| fetchButton.style.display = "none"; | |
| return; | |
| } | |
| // Now fetch reach for each ad | |
| let totalReach = 0; | |
| let adsFetched = 0; | |
| let adsWithData = 0; | |
| let noEuData = false; | |
| for (const groupAd of allAdsInGroup) { | |
| reachValue.textContent = `Calculating... (${adsFetched+1}/${allAdsInGroup.length})`; | |
| const reach = await fetchReachData(lastRequestIndex, groupAd.adArchiveID); | |
| adsFetched++; | |
| if (reach === "no_eu_data") { | |
| noEuData = true; | |
| } else if (reach && reach !== "auth_required") { | |
| totalReach += reach; | |
| adsWithData++; | |
| } | |
| } | |
| // Display the results | |
| if (noEuData) { | |
| reachValue.textContent = "No EU data available for this ad group"; | |
| } else if (adsWithData > 0) { | |
| reachValue.textContent = `${formatNumber(totalReach)} (across ${adsWithData} ads)`; | |
| } else { | |
| reachValue.textContent = "No reach data available for this ad group"; | |
| } | |
| // Hide the button | |
| fetchButton.style.display = "none"; | |
| } else { | |
| // Single ad - fetch reach directly | |
| const reach = await fetchReachData(lastRequestIndex, adID); | |
| if (reach === "no_eu_data") { | |
| reachValue.textContent = "No EU data available for this ad"; | |
| } else if (reach && reach !== "auth_required") { | |
| reachValue.textContent = formatNumber(reach); | |
| } else if (reach === "auth_required") { | |
| reachValue.textContent = "Please log in to view reach data"; | |
| } else { | |
| reachValue.textContent = "No reach data available for this ad"; | |
| } | |
| // Hide the button | |
| fetchButton.style.display = "none"; | |
| } | |
| } catch (error) { | |
| console.error("Error fetching reach data:", error); | |
| reachValue.textContent = "Error loading reach data"; | |
| // Reset button for retry | |
| fetchButton.disabled = false; | |
| fetchButton.textContent = "Try Again"; | |
| } | |
| }); | |
| // Add ad details link functionality | |
| adDetailsLink.addEventListener("click", () => { | |
| const collectionIndex = findCollectionIndex(adsCollections, adID); | |
| if (collectionIndex === -1) return; | |
| const adDetailsURL = `${new URL(window.location).origin}/ads/library/render_ad/?id=${adID}&access_token=`; | |
| // Open ad details in new tab | |
| window.open(adDetailsURL, '_blank'); | |
| }); | |
| // Add the reach info to the container | |
| container.appendChild(reachInfo); | |
| // Insert the container into the ad card | |
| const cardContent = card.children[0]; | |
| cardContent.insertBefore(container, cardContent.children[1]); | |
| return { reachInfo, reachValue, fetchButton }; | |
| } | |
| // Monitor the DOM for new ad cards and process them | |
| function monitorAdCards() { | |
| setInterval(() => { | |
| // Remove entries for cards that no longer exist in the DOM | |
| for (let i = 0; i < adCards.length; i++) { | |
| if (!document.body.contains(adCards[i].card)) { | |
| adCards.splice(i, 1); | |
| } | |
| } | |
| // Process all ad cards on the page | |
| let cards = document.querySelectorAll(".xh8yej3"); | |
| for (let i = 0; i < cards.length; i++) { | |
| // Get the ad ID from the card | |
| let idElement = cards[i].querySelector(".xt0e3qv > div > span"); | |
| if (idElement) { | |
| // Skip cards that aren't ad cards or have already been processed | |
| if (!(adCardClasses.includes(cards[i].className) || cards[i].className.includes("processed"))) continue; | |
| // Extract the ad ID | |
| let adID = idElement.textContent.split(": ")[1]; | |
| // Check if this card has already been processed | |
| if (!adCards.find(item => item.id === adID) && | |
| cards[i].parentElement.className.startsWith("xrvj5dj") && | |
| cards[i].parentElement.classList.length > 6) { | |
| // Create reach button and add to tracked cards | |
| let reachControls = createReachButton(cards[i], adID); | |
| if (reachControls) { | |
| adCards.push({ | |
| id: adID, | |
| card: cards[i], | |
| controls: reachControls | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| }, 1000); | |
| } | |
| // Find the collection index for an ad ID | |
| function findCollectionIndex(collections, adID) { | |
| for (let i = 0; i < collections.length; i++) { | |
| for (let j = 0; j < collections[i].length; j++) { | |
| if (collections[i][j].adArchiveID == adID) { | |
| return i; | |
| } | |
| } | |
| } | |
| return -1; | |
| } | |
| // Find the collection index by collation ID | |
| function findCollectionIndexByCollationID(collections, collationID) { | |
| for (let i = 0; i < collections.length; i++) { | |
| for (let j = 0; j < collections[i].length; j++) { | |
| if (collections[i][j].collationID == collationID) { | |
| return i; | |
| } | |
| } | |
| } | |
| return -1; | |
| } | |
| // Find the ad index within a collection | |
| function findAdIndex(collection, adID) { | |
| for (let i = 0; i < collection.length; i++) { | |
| if (collection[i].adArchiveID == adID) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| // Convert object to URL parameter string | |
| function toURLParams(obj) { | |
| let keys = Object.keys(obj); | |
| let params = new Array(); | |
| for (let i = 0; i < keys.length; i++) { | |
| params.push(`${keys[i]}=${obj[keys[i]]}`); | |
| } | |
| return params.join("&"); | |
| } | |
| // Set to track ongoing fetch requests | |
| let fetchRequests = new Set(); | |
| let collationFetchRequests = new Set(); | |
| // Fetch all ads in a collation group | |
| async function fetchAllAdsInCollation(requestIndex, collationID, collectedAds, totalAdsCount, forwardCursor = null) { | |
| // Skip if already fetching this collation | |
| if (collationFetchRequests.has(collationID)) return collectedAds; | |
| // Add to tracking set | |
| collationFetchRequests.add(collationID); | |
| return new Promise((resolve, reject) => { | |
| setTimeout(async () => { | |
| try { | |
| // Get URL parameters from current page | |
| let activeStatus = window.location.href.split("active_status=")[1].split("&")[0]; | |
| let country = window.location.href.split("country=")[1].split("&")[0]; | |
| let startDateMin, startDateMax, query; | |
| if (window.location.href.includes("start_date[min]=")) { | |
| startDateMin = window.location.href.split("start_date[min]=")[1].split("&")[0]; | |
| } | |
| if (window.location.href.includes("start_date[max]=")) { | |
| startDateMax = window.location.href.split("start_date[max]=")[1].split("&")[0]; | |
| } | |
| if (window.location.href.includes("&q")) { | |
| query = window.location.href.split("q=")[1].split("&")[0]; | |
| } | |
| // Construct URL for collation request | |
| let url = `${new URL(window.location).origin}/ads/library/async/collation/?collation_group_id=${collationID}&active_status=${activeStatus}&countries[0]=${country}`; | |
| if (startDateMin) url += `&start_date[min]=${startDateMin}`; | |
| if (startDateMax) url += `&start_date[max]=${startDateMax}`; | |
| if (query) url += `&q=${query}`; | |
| if (forwardCursor) url += `&forward_cursor=${forwardCursor}`; | |
| // Prepare request body | |
| requestParams.__req = reqID; | |
| requestParams.__s = sessionValue; | |
| requestParams.doc_id = docID; | |
| const requestBody = toURLParams(requestParams); | |
| // Make API request | |
| const response = await fetch(url, { | |
| headers: { | |
| "User-Agent": userAgent, | |
| "Accept": "*/*", | |
| "Accept-Language": "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| "X-FB-LSD": lsdToken, | |
| "X-ASBD-ID": asbdID, | |
| "Alt-Used": "www.facebook.com", | |
| "Sec-Fetch-Dest": "empty", | |
| "Sec-Fetch-Mode": "cors", | |
| "Sec-Fetch-Site": "same-origin", | |
| "Referer": referrerURL, | |
| "Origin": new URL(window.location).origin | |
| }, | |
| credentials: "same-origin", | |
| body: requestBody, | |
| method: "POST" | |
| }); | |
| // Parse response | |
| let responseText = await response.text(); | |
| let responseData = JSON.parse(responseText.replace("for (;;);", "")); | |
| // Make sure the request is still relevant | |
| if (requestIndex !== lastRequestIndex) return; | |
| // Remove from tracking set | |
| collationFetchRequests.delete(collationID); | |
| // Check for errors | |
| if (!responseData.payload) { | |
| resolve(collectedAds); | |
| return; | |
| } | |
| // Process ad cards from response | |
| if (responseData.payload.adCards) { | |
| // Add the new ads to our collection | |
| for (let i = 0; i < responseData.payload.adCards.length; i++) { | |
| collectedAds.push(responseData.payload.adCards[i]); | |
| // Add to our main data structure | |
| let collectionIndex = findCollectionIndexByCollationID(adsCollections, responseData.payload.adCards[0].collationID); | |
| if (collectionIndex === -1) { | |
| // This is a new collection we haven't seen before | |
| adsCollections.push([responseData.payload.adCards[i]]); | |
| } else { | |
| // Add to existing collection if not already there | |
| responseData.payload.adCards[i].adArchiveID = responseData.payload.adCards[i].adArchiveID.toString(); | |
| if (findAdIndex(adsCollections[collectionIndex], responseData.payload.adCards[i].adArchiveID) === -1) { | |
| adsCollections[collectionIndex].push(responseData.payload.adCards[i]); | |
| } | |
| } | |
| } | |
| // Check if we need to fetch more pages | |
| if (collectedAds.length === totalAdsCount || !responseData.payload.forwardCursor) { | |
| resolve(collectedAds); | |
| } else if (responseData.payload.forwardCursor) { | |
| // Fetch next page | |
| resolve(await fetchAllAdsInCollation( | |
| requestIndex, | |
| collationID, | |
| collectedAds, | |
| totalAdsCount, | |
| responseData.payload.forwardCursor | |
| )); | |
| } | |
| } else { | |
| resolve(collectedAds); | |
| } | |
| } catch (error) { | |
| console.error("Error fetching collation data:", error); | |
| collationFetchRequests.delete(collationID); | |
| resolve(collectedAds); // Return what we have so far | |
| } | |
| }, 1000 * (collationFetchRequests.size - 1)); // Stagger requests to avoid rate limiting | |
| }); | |
| } | |
| // Fetch reach data for an ad | |
| async function fetchReachData(requestIndex, adID) { | |
| // Skip if already fetching this ad | |
| if (fetchRequests.has(adID)) return new Promise(resolve => { | |
| // Poll until the request completes | |
| const checkInterval = setInterval(() => { | |
| if (!fetchRequests.has(adID)) { | |
| clearInterval(checkInterval); | |
| const collectionIndex = findCollectionIndex(adsCollections, adID); | |
| if (collectionIndex === -1) { | |
| resolve(false); | |
| return; | |
| } | |
| const adIndex = findAdIndex(adsCollections[collectionIndex], adID); | |
| if (adIndex === -1) { | |
| resolve(false); | |
| return; | |
| } | |
| resolve(adsCollections[collectionIndex][adIndex].eu_total_reach || false); | |
| } | |
| }, 100); | |
| }); | |
| // Add to tracking set and create fetching promise | |
| fetchRequests.add(adID); | |
| return new Promise((resolve, reject) => { | |
| setTimeout(async () => { | |
| isFetching = true; | |
| try { | |
| // Find collection and ad indexes | |
| const collectionIndex = findCollectionIndex(adsCollections, adID); | |
| if (collectionIndex === -1) { | |
| fetchRequests.delete(adID); | |
| isFetching = false; | |
| resolve(false); | |
| return; | |
| } | |
| // Get ad data and page ID | |
| const adIndex = findAdIndex(adsCollections[collectionIndex], adID); | |
| if (adIndex === -1) { | |
| fetchRequests.delete(adID); | |
| isFetching = false; | |
| resolve(false); | |
| return; | |
| } | |
| const ad = adsCollections[collectionIndex][adIndex]; | |
| // Check if we already have reach data | |
| if (ad.eu_total_reach) { | |
| fetchRequests.delete(adID); | |
| isFetching = false; | |
| resolve(ad.eu_total_reach); | |
| return; | |
| } | |
| // Check if ad is not AAA eligible (not subject to EU transparency rules) | |
| if (ad.is_aaa_eligible === false) { | |
| fetchRequests.delete(adID); | |
| isFetching = false; | |
| resolve("no_eu_data"); | |
| return; | |
| } | |
| // Make GraphQL request for ad reach data | |
| let response; | |
| // Construct request body | |
| requestParams.__req = reqID; | |
| requestParams.__s = sessionValue; | |
| requestParams.variables = JSON.stringify({ | |
| adArchiveID: adID, | |
| pageID: ad.pageID, | |
| country: "ALL", | |
| sessionID: sessionID, | |
| source: null, | |
| isAdNonPolitical: true, | |
| isAdNotAAAEligible: false | |
| }); | |
| requestParams.doc_id = docID; | |
| const requestBody = toURLParams(requestParams); | |
| // Make the API request | |
| response = await fetch(`${new URL(window.location).origin}/api/graphql/`, { | |
| headers: { | |
| "User-Agent": userAgent, | |
| "Accept": "*/*", | |
| "Accept-Language": "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| "X-FB-Friendly-Name": "AdLibraryAdDetailsV2Query", | |
| "X-FB-LSD": lsdToken, | |
| "X-ASBD-ID": asbdID, | |
| "Alt-Used": "www.facebook.com", | |
| "Sec-Fetch-Dest": "empty", | |
| "Sec-Fetch-Mode": "cors", | |
| "Sec-Fetch-Site": "same-origin", | |
| "Referer": referrerURL, | |
| "Origin": new URL(window.location).origin | |
| }, | |
| credentials: "same-origin", | |
| body: requestBody, | |
| method: "POST" | |
| }); | |
| // Process response | |
| let responseData = await response.json(); | |
| // Make sure the request is still relevant | |
| if (requestIndex !== lastRequestIndex) return; | |
| // Remove from tracking set | |
| fetchRequests.delete(adID); | |
| // Update data with reach information | |
| if (responseData.data) { | |
| // Check if aaa_info exists at all | |
| if (!responseData.data.ad_library_main.ad_details.aaa_info) { | |
| // No aaa_info means this ad is not subject to EU transparency rules | |
| adsCollections[collectionIndex][adIndex].is_aaa_eligible = false; | |
| isFetching = false; | |
| resolve("no_eu_data"); | |
| return; | |
| } | |
| const reachData = responseData.data.ad_library_main.ad_details.aaa_info.eu_total_reach; | |
| // Update the ad object with the reach data | |
| if (reachData) { | |
| adsCollections[collectionIndex][adIndex].eu_total_reach = reachData; | |
| adsCollections[collectionIndex][adIndex].is_aaa_eligible = true; | |
| isFetching = false; | |
| resolve(reachData); | |
| } else { | |
| // EU total reach might be null even if aaa_info exists | |
| adsCollections[collectionIndex][adIndex].is_aaa_eligible = false; | |
| isFetching = false; | |
| resolve("no_eu_data"); | |
| } | |
| } else if (responseData.errors || responseData.error) { | |
| isFetching = false; | |
| resolve("auth_required"); | |
| } else { | |
| isFetching = false; | |
| resolve(false); | |
| } | |
| } catch (error) { | |
| console.error("Error fetching reach data:", error); | |
| isFetching = false; | |
| fetchRequests.delete(adID); | |
| resolve(false); | |
| } | |
| }, 1000 * (fetchRequests.size - 1)); // Stagger requests to avoid rate limiting | |
| }); | |
| } | |
| // Monitor for page changes | |
| setInterval(() => { | |
| if (window.location.href !== currentURL) { | |
| currentURL = window.location.href; | |
| adCards = new Array(); | |
| monitorAdCards(); | |
| fetchRequests.clear(); | |
| collationFetchRequests.clear(); | |
| lastRequestIndex++; | |
| } | |
| }, 50); | |
| // Start monitoring for ad cards | |
| monitorAdCards(); | |
| // Intercept XHR requests to extract necessary data | |
| const originalXHROpen = XMLHttpRequest.prototype.open; | |
| XMLHttpRequest.prototype.open = function(method, url, ...args) { | |
| this._method = method; | |
| this._url = url; | |
| this._headers = {}; | |
| originalXHROpen.apply(this, [method, url, ...args]); | |
| }; | |
| const originalXHRSetHeader = XMLHttpRequest.prototype.setRequestHeader; | |
| XMLHttpRequest.prototype.setRequestHeader = function(name, value) { | |
| this._headers[name] = value; | |
| originalXHRSetHeader.apply(this, [name, value]); | |
| }; | |
| // Helper to rename object properties | |
| let renameProperty = (obj, oldName, newName) => { | |
| obj[newName] = obj[oldName]; | |
| return obj; | |
| }; | |
| // Intercept XHR responses to extract data | |
| const originalXHRSend = XMLHttpRequest.prototype.send; | |
| XMLHttpRequest.prototype.send = function(data) { | |
| this._data = data; | |
| this.addEventListener("load", function() { | |
| // Only process requests on the Ad Library page | |
| if (window.location.href.includes("facebook.com/ads/library/?")) { | |
| // Process GraphQL requests | |
| if (this._url === "/api/graphql/") { | |
| // Extract tokens from headers | |
| if (this._headers["X-FB-LSD"]) { | |
| lsdToken = this._headers["X-FB-LSD"]; | |
| } | |
| if (this._headers["X-ASBD-ID"]) { | |
| asbdID = this._headers["X-ASBD-ID"]; | |
| } | |
| // Parse request parameters | |
| try { | |
| let requestData = JSON.parse('{"' + this._data.replace(/&/g, '","').replace(/=/g, '":"') + '"}', | |
| function(key, value) { | |
| return key === "" ? value : decodeURIComponent(value); | |
| } | |
| ); | |
| // Update request counter for sequencing | |
| reqID = (parseInt(requestData.__req, 36) + 1).toString(36); | |
| sessionValue = requestData.__s; | |
| // Extract session ID | |
| let variablesObj = JSON.parse(requestData.variables); | |
| if (variablesObj.session_id) { | |
| sessionID = variablesObj.session_id; | |
| } else if (variablesObj.sessionId) { | |
| sessionID = variablesObj.sessionId; | |
| } else if (variablesObj.sessionID) { | |
| sessionID = variablesObj.sessionID; | |
| } | |
| // Store document ID for the ad details query | |
| if (requestData.fb_api_req_friendly_name && | |
| requestData.fb_api_req_friendly_name[0] === "AdLibraryAdDetailsV2Query" && | |
| !docID) { | |
| docID = requestData.doc_id; | |
| } | |
| // Create request parameters object | |
| requestParams = new RequestParameters( | |
| requestData.__user, | |
| requestData.__a, | |
| requestData.__hs, | |
| requestData.dpr, | |
| requestData.__ccg, | |
| requestData.__rev, | |
| requestData.__hsi, | |
| requestData.__dyn, | |
| requestData.__csr, | |
| requestData.fb_dtsg, | |
| requestData.jazoest, | |
| requestData.lsd, | |
| requestData.__aaid, | |
| requestData.__spin_r, | |
| requestData.__spin_b, | |
| requestData.__spin_t, | |
| requestData.__jssesw | |
| ); | |
| // Process ad data from pagination queries | |
| if (requestData.fb_api_req_friendly_name === "AdLibrarySearchPaginationQuery" || | |
| requestData.fb_api_req_friendly_name === "AdLibraryMobileFocusedStateProviderQuery" || | |
| requestData.fb_api_req_friendly_name === "AdLibraryMobileFocusedStateProviderRefetchQuery") { | |
| let responseData; | |
| try { | |
| responseData = JSON.parse(this.responseText); | |
| } catch { | |
| if (this.responseText.includes("\n") && | |
| requestData.fb_api_req_friendly_name != "AdLibraryMobileFocusedStateProviderRefetchQuery") { | |
| responseData = JSON.parse(this.responseText.split("\n")[0]); | |
| } else if (this.responseText.includes("\n") && | |
| requestData.fb_api_req_friendly_name === "AdLibraryMobileFocusedStateProviderRefetchQuery") { | |
| responseData = JSON.parse(this.responseText.split("\n")[1]); | |
| } else { | |
| return; | |
| } | |
| } | |
| if (!responseData.data || | |
| !responseData.data.ad_library_main || | |
| !responseData.data.ad_library_main.search_results_connection || | |
| !responseData.data.ad_library_main.search_results_connection.edges) { | |
| return; | |
| } | |
| // Process ad collections | |
| let collections = responseData.data.ad_library_main.search_results_connection.edges.map( | |
| edge => edge.node.collated_results | |
| ); | |
| // Process each ad collection | |
| for (let i = 0; i < collections.length; i++) { | |
| for (let j = 0; j < collections[i].length; j++) { | |
| // Normalize property names | |
| if (collections[i][j].hasOwnProperty("ad_archive_id")) { | |
| collections[i][j] = renameProperty(collections[i][j], "ad_archive_id", "adArchiveID"); | |
| } | |
| if (collections[i][j].hasOwnProperty("collation_id")) { | |
| collections[i][j] = renameProperty(collections[i][j], "collation_id", "collationID"); | |
| } | |
| if (collections[i][j].hasOwnProperty("end_date")) { | |
| collections[i][j] = renameProperty(collections[i][j], "end_date", "endDate"); | |
| } | |
| if (collections[i][j].hasOwnProperty("page_id")) { | |
| collections[i][j] = renameProperty(collections[i][j], "page_id", "pageID"); | |
| } | |
| if (collections[i][j].hasOwnProperty("page_name")) { | |
| collections[i][j] = renameProperty(collections[i][j], "page_name", "pageName"); | |
| } | |
| if (collections[i][j].hasOwnProperty("start_date")) { | |
| collections[i][j] = renameProperty(collections[i][j], "start_date", "startDate"); | |
| } | |
| if (collections[i][j].hasOwnProperty("collation_count")) { | |
| collections[i][j] = renameProperty(collections[i][j], "collation_count", "collationCount"); | |
| } | |
| } | |
| // Add collection if not already present | |
| if (findCollectionIndex(adsCollections, collections[i][0].adArchiveID) === -1) { | |
| adsCollections.push(collections[i]); | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Error processing GraphQL request data:", error); | |
| } | |
| } | |
| // Process async search results | |
| else if (this._url.startsWith("/ads/library/async/search_ads/")) { | |
| try { | |
| let responseData = JSON.parse(this.responseText.replace("for (;;);", "")); | |
| if (!responseData.payload || !responseData.payload.results) return; | |
| // Process each result | |
| let results = responseData.payload.results; | |
| for (let i = 0; i < results.length; i++) { | |
| // Add collection if not already present | |
| if (findCollectionIndex(adsCollections, results[i][0].adArchiveID) === -1) { | |
| adsCollections.push(results[i]); | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Error processing async search results:", error); | |
| } | |
| } | |
| // Process async collation results | |
| else if (this._url.startsWith("/ads/library/async/collation/")) { | |
| try { | |
| let responseData = JSON.parse(this.responseText.replace("for (;;);", "")); | |
| if (!responseData.payload || !responseData.payload.adCards) return; | |
| let adCards = responseData.payload.adCards; | |
| // Find collection for these cards | |
| let collectionIndex = findCollectionIndexByCollationID(adsCollections, adCards[0].collationID); | |
| // Add cards to collection | |
| for (let i = 0; i < adCards.length; i++) { | |
| adCards[i].adArchiveID = adCards[i].adArchiveID.toString(); | |
| if (collectionIndex !== -1) { | |
| if (findAdIndex(adsCollections[collectionIndex], adCards[i].adArchiveID) === -1) { | |
| adsCollections[collectionIndex].push(adCards[i]); | |
| } | |
| } else { | |
| // Create a new collection | |
| adsCollections.push([adCards[i]]); | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Error processing async collation results:", error); | |
| } | |
| } | |
| } | |
| }); | |
| originalXHRSend.apply(this, [data]); | |
| }; | |
| // Parse initial data from page on load | |
| let initialDataInterval = setInterval(() => { | |
| if (document.body && | |
| document.body.innerHTML.includes("search_results_connection") && | |
| document.body.innerHTML.includes("page_info")) { | |
| clearInterval(initialDataInterval); | |
| setTimeout(() => { | |
| try { | |
| // Extract initial data from page HTML | |
| let initialData = JSON.parse( | |
| document.body.innerHTML.split('search_results_connection":')[1].split(',"page_info')[0] + "}" | |
| ); | |
| // Process collections from initial data | |
| initialData = initialData.edges.map(edge => edge.node.collated_results); | |
| for (let i = 0; i < initialData.length; i++) { | |
| for (let j = 0; j < initialData[i].length; j++) { | |
| // Normalize property names | |
| if (initialData[i][j].hasOwnProperty("ad_archive_id")) { | |
| initialData[i][j] = renameProperty(initialData[i][j], "ad_archive_id", "adArchiveID"); | |
| } | |
| if (initialData[i][j].hasOwnProperty("collation_id")) { | |
| initialData[i][j] = renameProperty(initialData[i][j], "collation_id", "collationID"); | |
| } | |
| if (initialData[i][j].hasOwnProperty("end_date")) { | |
| initialData[i][j] = renameProperty(initialData[i][j], "end_date", "endDate"); | |
| } | |
| if (initialData[i][j].hasOwnProperty("page_id")) { | |
| initialData[i][j] = renameProperty(initialData[i][j], "page_id", "pageID"); | |
| } | |
| if (initialData[i][j].hasOwnProperty("page_name")) { | |
| initialData[i][j] = renameProperty(initialData[i][j], "page_name", "pageName"); | |
| } | |
| if (initialData[i][j].hasOwnProperty("start_date")) { | |
| initialData[i][j] = renameProperty(initialData[i][j], "start_date", "startDate"); | |
| } | |
| if (initialData[i][j].hasOwnProperty("collation_count")) { | |
| initialData[i][j] = renameProperty(initialData[i][j], "collation_count", "collationCount"); | |
| } | |
| } | |
| // Add collection if not already present | |
| if (findCollectionIndex(adsCollections, initialData[i][0].adArchiveID) === -1) { | |
| adsCollections.push(initialData[i]); | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Error processing initial page data:", error); | |
| } | |
| }, 1000); | |
| } | |
| }, 500); | |
| // Return API | |
| return { | |
| getReachForAd: async (adID) => { | |
| const collectionIndex = findCollectionIndex(adsCollections, adID); | |
| if (collectionIndex === -1) return null; | |
| const adIndex = findAdIndex(adsCollections[collectionIndex], adID); | |
| if (adIndex === -1) return null; | |
| const ad = adsCollections[collectionIndex][adIndex]; | |
| if (ad.eu_total_reach) { | |
| return ad.eu_total_reach; | |
| } else { | |
| return await fetchReachData(lastRequestIndex, adID); | |
| } | |
| }, | |
| getReachForCollation: async (collationID) => { | |
| const collectionIndex = findCollectionIndexByCollationID(adsCollections, collationID); | |
| if (collectionIndex === -1) return null; | |
| const ads = adsCollections[collectionIndex]; | |
| const collationCount = ads[0].collationCount || ads.length; | |
| // Fetch all ads in the collation if needed | |
| let allAds = ads; | |
| if (ads.length < collationCount) { | |
| allAds = await fetchAllAdsInCollation(lastRequestIndex, collationID, [], collationCount); | |
| } | |
| // Fetch reach for each ad | |
| let totalReach = 0; | |
| let adsWithData = 0; | |
| for (const ad of allAds) { | |
| const reach = await fetchReachData(lastRequestIndex, ad.adArchiveID); | |
| if (reach && reach !== "auth_required") { | |
| totalReach += reach; | |
| adsWithData++; | |
| } | |
| } | |
| return { | |
| totalReach, | |
| adsWithData, | |
| totalAds: allAds.length | |
| }; | |
| }, | |
| getAllAds: () => { | |
| return adsCollections.flatMap(collection => | |
| collection.map(ad => ({ | |
| id: ad.adArchiveID, | |
| pageID: ad.pageID, | |
| pageName: ad.pageName, | |
| reach: ad.eu_total_reach || null, | |
| isCollation: ad.collationCount && ad.collationCount > 1, | |
| collationID: ad.collationID, | |
| collationCount: ad.collationCount | |
| })) | |
| ); | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment