Initial commit: moreminimore-service content pipeline
- 9 mm* skills (orchestrator, article, social, publish, analytics) - 6 dependency skills (content-writer, geo-optimizer, etc.) - 3 analytics scripts (GSC, Google Ads, Meta Ads) - Config template + setup guide - SOUL-MM.md persona extension - OrbitOS integration reference
This commit is contained in:
303
scripts/mm_meta_ads.py
Normal file
303
scripts/mm_meta_ads.py
Normal file
@@ -0,0 +1,303 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user