import requests import os import time import logging from typing import Dict, List, Any, Optional from datetime import datetime, timedelta from pathlib import Path import json class FacebookAdLibraryClient: """ Facebook Ad Library API Client Handles authentication, ad searching, and creative asset downloading from Facebook's Ad Library API. """ def __init__(self, app_id: str = None, app_secret: str = None, access_token: str = None): print("๐Ÿ”— Initializing Facebook Ad Library Client...") # API configuration self.base_url = "https://graph.facebook.com/v19.0" self.ad_library_url = f"{self.base_url}/ads_archive" # Authentication self.app_id = app_id or os.getenv('FACEBOOK_APP_ID') self.app_secret = app_secret or os.getenv('FACEBOOK_APP_SECRET') self.access_token = access_token or os.getenv('FACEBOOK_ACCESS_TOKEN') # Rate limiting self.requests_per_hour = 200 # Facebook's default rate limit self.request_count = 0 self.rate_limit_reset = time.time() + 3600 # Supported countries (Ad Library API limitation) self.supported_countries = { 'US': 'United States', 'GB': 'United Kingdom', 'BR': 'Brazil', 'EU': 'European Union', 'ALL': 'Global (Political ads only)' } self._validate_credentials() print(" โœ… Facebook Ad Library Client ready") def _validate_credentials(self): """Validate API credentials""" missing = [] if not self.app_id: missing.append('FACEBOOK_APP_ID') if not self.app_secret: missing.append('FACEBOOK_APP_SECRET') if missing: print(f" โš ๏ธ Missing credentials: {', '.join(missing)}") print(" ๐Ÿ’ก Set environment variables or pass them to the constructor") return False # Generate access token if not provided if not self.access_token: print(" ๐Ÿ”‘ Generating app access token...") self.access_token = self._generate_app_access_token() return True def _generate_app_access_token(self) -> str: """Generate app access token for API requests""" try: url = f"{self.base_url}/oauth/access_token" params = { 'grant_type': 'client_credentials', 'client_id': self.app_id, 'client_secret': self.app_secret } response = requests.get(url, params=params) response.raise_for_status() token_data = response.json() return token_data.get('access_token') except Exception as e: print(f" โŒ Failed to generate access token: {e}") return None def _check_rate_limit(self): """Check and enforce API rate limiting""" current_time = time.time() # Reset counter if hour has passed if current_time > self.rate_limit_reset: self.request_count = 0 self.rate_limit_reset = current_time + 3600 # Check if we've hit the limit if self.request_count >= self.requests_per_hour: sleep_time = self.rate_limit_reset - current_time if sleep_time > 0: print(f" โณ Rate limit reached, sleeping for {sleep_time:.0f} seconds...") time.sleep(sleep_time + 1) self.request_count = 0 self.rate_limit_reset = time.time() + 3600 def search_ads(self, search_terms: str = None, ad_reached_countries: str = 'US', ad_type: str = 'ALL', media_type: str = 'ALL', start_date: str = None, end_date: str = None, limit: int = 100) -> Dict[str, Any]: """ Search for ads in the Facebook Ad Library Args: search_terms: Keywords to search for ad_reached_countries: Country codes (US, GB, BR, EU, ALL) ad_type: ALL, POLITICAL_AND_ISSUE_ADS, or REGULAR media_type: ALL, VIDEO, IMAGE start_date: YYYY-MM-DD format end_date: YYYY-MM-DD format limit: Number of results to return (max 5000) Returns: Dict containing ad search results """ print(f"๐Ÿ” Searching Facebook ads...") print(f" Terms: {search_terms or 'Any'}") print(f" Country: {ad_reached_countries}") print(f" Type: {ad_type}") if not self.access_token: return {"error": "No access token available"} self._check_rate_limit() # Build query parameters params = { 'access_token': self.access_token, 'ad_reached_countries': ad_reached_countries, 'ad_type': ad_type, 'limit': min(limit, 5000), # API maximum 'fields': 'id,ad_creation_time,ad_creative_bodies,ad_creative_link_captions,ad_creative_link_descriptions,ad_creative_link_titles,ad_delivery_start_time,ad_delivery_stop_time,ad_snapshot_url,currency,demographic_distribution,funding_entity,impressions,page_id,page_name,publisher_platforms,spend' } # Add optional parameters if search_terms: params['search_terms'] = search_terms if media_type and media_type != 'ALL': # Facebook API expects specific values for media_type media_mapping = { 'VIDEO': 'VIDEO', 'IMAGE': 'IMAGE' } if media_type in media_mapping: params['media_type'] = media_mapping[media_type] if start_date: params['ad_delivery_date_min'] = start_date if end_date: params['ad_delivery_date_max'] = end_date try: response = requests.get(self.ad_library_url, params=params) self.request_count += 1 response.raise_for_status() data = response.json() ads_found = len(data.get('data', [])) print(f" โœ… Found {ads_found} ads") return data except requests.exceptions.HTTPError as e: error_data = {} try: error_data = response.json() except: pass print(f" โŒ API Error: {e}") if error_data.get('error'): error_msg = error_data['error'].get('message', 'Unknown error') error_type = error_data['error'].get('type', 'Unknown') error_code = error_data['error'].get('code', 'Unknown') print(f" Error details: {error_msg} (Type: {error_type}, Code: {error_code})") print(f" Full error response: {error_data}") # Provide helpful troubleshooting suggestions if error_type == 'OAuthException': print(" ๐Ÿ’ก This is an authentication/permission error:") print(" - Verify your App ID and App Secret are correct") print(" - Check if your Facebook app is approved for Ad Library API access") print(" - Ensure you have completed identity verification") print(" - Try accessing Facebook's Ad Library directly at facebook.com/ads/library") return {"error": f"API request failed: {e}", "details": error_data} except Exception as e: print(f" โŒ Request failed: {e}") return {"error": f"Request failed: {e}"} def download_ad_creative(self, ad_snapshot_url: str, output_dir: str, ad_id: str) -> Dict[str, Any]: """ Download creative assets from an ad snapshot URL Args: ad_snapshot_url: URL to the ad snapshot output_dir: Directory to save downloaded assets ad_id: Unique identifier for the ad Returns: Dict with download results and file paths """ print(f"๐Ÿ“ฅ Downloading creative for ad {ad_id}...") try: # Create output directory ad_dir = Path(output_dir) / f"ad_{ad_id}" ad_dir.mkdir(parents=True, exist_ok=True) # For now, save the snapshot URL and metadata # Note: Facebook's Ad Library API doesn't directly provide creative assets # The snapshot URL leads to a webpage, not direct media files metadata = { "ad_id": ad_id, "snapshot_url": ad_snapshot_url, "download_time": datetime.now().isoformat(), "status": "url_saved", "note": "Direct creative download not available via API - snapshot URL saved" } # Save metadata metadata_file = ad_dir / "metadata.json" with open(metadata_file, 'w') as f: json.dump(metadata, f, indent=2) print(f" โœ… Metadata saved to {metadata_file}") return { "success": True, "ad_id": ad_id, "metadata_file": str(metadata_file), "snapshot_url": ad_snapshot_url, "note": "API limitation: Creative assets require web scraping for direct download" } except Exception as e: print(f" โŒ Download failed: {e}") return { "success": False, "ad_id": ad_id, "error": str(e) } def get_ad_insights(self, ad_id: str) -> Dict[str, Any]: """ Get detailed insights for a specific ad (if available) Args: ad_id: Facebook ad ID Returns: Dict containing ad insights """ print(f"๐Ÿ“Š Getting insights for ad {ad_id}...") if not self.access_token: return {"error": "No access token available"} self._check_rate_limit() try: url = f"{self.base_url}/{ad_id}" params = { 'access_token': self.access_token, 'fields': 'id,name,status,created_time,updated_time' } response = requests.get(url, params=params) self.request_count += 1 response.raise_for_status() return response.json() except Exception as e: print(f" โŒ Insights request failed: {e}") return {"error": f"Failed to get insights: {e}"} def test_connection(self) -> Dict[str, Any]: """ Test API connection and permissions Returns: Dict with connection test results """ print("๐Ÿงช Testing Facebook Ad Library API connection...") if not self.access_token: return {"success": False, "error": "No access token available"} try: # Simple test query - search for political ads in the US (broader access) result = self.search_ads( ad_reached_countries='US', ad_type='POLITICAL_AND_ISSUE_ADS', limit=1 ) if "error" in result: return {"success": False, "error": result["error"]} ads_count = len(result.get('data', [])) return { "success": True, "message": f"Connection successful! Found {ads_count} ads in test query", "rate_limit_remaining": self.requests_per_hour - self.request_count, "supported_countries": list(self.supported_countries.keys()) } except Exception as e: return {"success": False, "error": f"Connection test failed: {e}"} def create_facebook_client(**kwargs) -> FacebookAdLibraryClient: """ Factory function to create a Facebook Ad Library client Args: **kwargs: Arguments to pass to FacebookAdLibraryClient constructor Returns: Configured FacebookAdLibraryClient instance """ return FacebookAdLibraryClient(**kwargs)