#!/usr/bin/env python3 """ Meta Ads API for moreminimore-service. Reads clients-config.json, pulls campaign/ad set/ad data. Usage: python3 mm_meta_ads.py --client moreminimore campaigns python3 mm_meta_ads.py --client moreminimore adsets python3 mm_meta_ads.py --client moreminimore stats --days 30 """ import argparse import json import os import sys import urllib.request import urllib.error import urllib.parse from datetime import datetime, timedelta def load_config(): """Load clients-config.json""" config_path = os.path.expanduser("~/vault/99_System/clients-config.json") with open(config_path) as f: return json.load(f) def find_client(config, client_id): """Find client by ID""" for client in config.get("clients", []): if client["id"] == client_id: return client return None def get_meta_config(config, client): """Get Meta Ads credentials from config""" meta = config.get("global", {}).get("meta", {}) ad_account_id = client.get("analytics", {}).get("meta_ad_account_id", "") page_id = client.get("social", {}).get("facebook", {}).get("page_id", "") page_token = client.get("social", {}).get("facebook", {}).get("page_token", "") system_user_token = meta.get("system_user_token", "") # Use page_token for page operations, system_user_token for ad operations token = page_token or system_user_token if not token: print("ERROR: No page_token or system_user_token in config", file=sys.stderr) sys.exit(1) if not ad_account_id: print(f"ERROR: No meta_ad_account_id for client '{client['id']}'", file=sys.stderr) sys.exit(1) return { "token": token, "ad_account_id": ad_account_id, "page_id": page_id, } def meta_api_get(endpoint, params=None): """Make a GET request to Meta Graph API""" base_url = "https://graph.facebook.com/v21.0" url = f"{base_url}/{endpoint}" if params: url += "?" + urllib.parse.urlencode(params) req = urllib.request.Request(url) try: with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as e: error_body = e.read().decode() if e.fp else "" print(f"ERROR: Meta API returned {e.code}", file=sys.stderr) if error_body: try: error_json = json.loads(error_body) print(json.dumps(error_json, indent=2), file=sys.stderr) except: print(error_body[:500], file=sys.stderr) sys.exit(1) def meta_api_post(endpoint, data): """Make a POST request to Meta Graph API""" base_url = "https://graph.facebook.com/v21.0" url = f"{base_url}/{endpoint}" body = urllib.parse.urlencode(data).encode() req = urllib.request.Request(url, data=body, method="POST") try: with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as e: error_body = e.read().decode() if e.fp else "" print(f"ERROR: Meta API returned {e.code}", file=sys.stderr) if error_body: try: error_json = json.loads(error_body) print(json.dumps(error_json, indent=2), file=sys.stderr) except: print(error_body[:500], file=sys.stderr) sys.exit(1) def list_campaigns(meta_config): """List active campaigns""" result = meta_api_get( f"act_{meta_config['ad_account_id']}/campaigns", { "fields": "id,name,status,objective,daily_budget,lifetime_budget,created_time", "filtering": json.dumps([{"field": "effective_status", "operator": "IN", "value": ["ACTIVE"]}]), "access_token": meta_config["token"], } ) return result def list_adsets(meta_config): """List active ad sets""" result = meta_api_get( f"act_{meta_config['ad_account_id']}/adsets", { "fields": "id,name,status,targeting,daily_budget,lifetime_budget,optimization_goal,billing_event,created_time", "filtering": json.dumps([{"field": "effective_status", "operator": "IN", "value": ["ACTIVE"]}]), "access_token": meta_config["token"], } ) return result def get_insights(meta_config, days=30, level="campaign"): """Get performance insights""" date_start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") date_end = datetime.now().strftime("%Y-%m-%d") result = meta_api_get( f"act_{meta_config['ad_account_id']}/insights", { "level": level, "fields": "campaign_name,adset_name,impressions,clicks,spend,actions,cpm,cpc,ctr,reach,frequency", "time_range": json.dumps({"since": date_start, "until": date_end}), "time_increment": "1", "access_token": meta_config["token"], } ) return result def format_currency(amount): """Format currency""" if not amount: return "฿0" return f"฿{float(amount):,.2f}" def format_number(n): """Format number with commas""" if not n: return "0" return f"{int(n):,}" def display_campaigns(data): """Display campaign data""" if not data or not data.get("data"): print("ไม่พบ campaign ที่ active") return campaigns = data["data"] print(f"\n📋 Active Campaigns ({len(campaigns)})") print("-" * 90) print(f"{'Name':<35} {'Objective':<20} {'Daily Budget':>15} {'Status':<10}") print("-" * 90) for c in campaigns: name = c.get("name", "")[:33] objective = c.get("objective", "") daily_budget = c.get("daily_budget", "") status = c.get("status", "") budget_str = f"฿{int(daily_budget)/100:,.2f}" if daily_budget else "-" print(f"{name:<35} {objective:<20} {budget_str:>15} {status:<10}") def display_adsets(data): """Display ad set data""" if not data or not data.get("data"): print("ไม่พบ ad set ที่ active") return adsets = data["data"] print(f"\n📋 Active Ad Sets ({len(adsets)})") print("-" * 100) print(f"{'Name':<30} {'Optimization':<20} {'Billing':<15} {'Daily Budget':>15}") print("-" * 100) for a in adsets: name = a.get("name", "")[:28] opt_goal = a.get("optimization_goal", "") billing = a.get("billing_event", "") daily_budget = a.get("daily_budget", "") budget_str = f"฿{int(daily_budget)/100:,.2f}" if daily_budget else "-" print(f"{name:<30} {opt_goal:<20} {billing:<15} {budget_str:>15}") def display_stats(data, level="campaign"): """Display performance stats""" if not data or not data.get("data"): print("ไม่พบข้อมูลสถิติ") return rows = data["data"] # Aggregate by campaign or adset aggregated = {} for row in rows: key = row.get("campaign_name", "") if level == "campaign" else row.get("adset_name", "") if key not in aggregated: aggregated[key] = { "impressions": 0, "clicks": 0, "spend": 0, "reach": 0 } aggregated[key]["impressions"] += int(row.get("impressions", 0)) aggregated[key]["clicks"] += int(row.get("clicks", 0)) aggregated[key]["spend"] += float(row.get("spend", 0)) aggregated[key]["reach"] += int(row.get("reach", 0)) label = "Campaign" if level == "campaign" else "Ad Set" print(f"\n📊 {label} Stats") print("-" * 100) print(f"{label:<30} {'Impressions':>12} {'Clicks':>10} {'Spend':>12} {'Reach':>12} {'CTR':>8} {'CPC':>10}") print("-" * 100) for name, m in aggregated.items(): ctr = (m["clicks"] / m["impressions"] * 100) if m["impressions"] > 0 else 0 cpc = (m["spend"] / m["clicks"]) if m["clicks"] > 0 else 0 print(f"{name[:28]:<30} {format_number(m['impressions']):>12} " f"{format_number(m['clicks']):>10} " f"{format_currency(m['spend']):>12} " f"{format_number(m['reach']):>12} " f"{ctr:>7.2f}% " f"{format_currency(cpc):>10}") def main(): parser = argparse.ArgumentParser(description="Meta Ads API for moreminimore-service") parser.add_argument("--client", required=True, help="Client ID from clients-config.json") parser.add_argument("action", choices=["campaigns", "adsets", "stats"], help="Action to perform") parser.add_argument("--days", type=int, default=30, help="Number of days for stats (default: 30)") parser.add_argument("--level", choices=["campaign", "adset"], default="campaign", help="Stats level") parser.add_argument("--json", action="store_true", help="Output raw JSON") args = parser.parse_args() # Load config config = load_config() # Find client client = find_client(config, args.client) if not client: print(f"ERROR: Client '{args.client}' not found", file=sys.stderr) sys.exit(1) # Get Meta config meta_config = get_meta_config(config, client) print(f"📊 Meta Ads — {client['display_name']}") print(f" Ad Account: {meta_config['ad_account_id']}") print() # Execute action if args.action == "campaigns": data = list_campaigns(meta_config) if args.json: print(json.dumps(data, indent=2)) else: display_campaigns(data) elif args.action == "adsets": data = list_adsets(meta_config) if args.json: print(json.dumps(data, indent=2)) else: display_adsets(data) elif args.action == "stats": data = get_insights(meta_config, args.days, args.level) if args.json: print(json.dumps(data, indent=2)) else: display_stats(data, args.level) if __name__ == "__main__": main()