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:
Kunthawat Greethong
2026-07-02 10:01:53 +07:00
commit 7b5af16f6d
36 changed files with 7803 additions and 0 deletions

303
scripts/mm_meta_ads.py Normal file
View 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()