Skip to content

Instantly share code, notes, and snippets.

@saas-coder
Last active March 14, 2025 20:20
Show Gist options
  • Select an option

  • Save saas-coder/22801e738de5695041270692959c23da to your computer and use it in GitHub Desktop.

Select an option

Save saas-coder/22801e738de5695041270692959c23da to your computer and use it in GitHub Desktop.
// 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