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:
87
scripts/README.md
Normal file
87
scripts/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Platform API Scripts — คู่มือการติดตั้ง
|
||||
|
||||
scripts/ เหล่านี้เป็น bridge สำหรับเชื่อมต่อกับ platform APIs
|
||||
ณ ตอนนี้เป็น **placeholder** — ต้องติดตั้ง dependencies ก่อนใช้งาน
|
||||
|
||||
## Prerequisites (ทั่วไป)
|
||||
|
||||
```bash
|
||||
# Hermes ต้องมี tools เหล่านี้
|
||||
# web_search สำหรับค้นหาข้อมูล
|
||||
# terminal สำหรับรัน scripts
|
||||
```
|
||||
|
||||
## Google Ads API
|
||||
|
||||
### Dependencies
|
||||
```bash
|
||||
pip install google-ads
|
||||
```
|
||||
|
||||
### Authentication
|
||||
1. ไปที่ https://console.cloud.google.com/ → สร้าง Project
|
||||
2. Enable Google Ads API
|
||||
3. สร้าง OAuth 2.0 credentials → ดาวน์โหลด JSON
|
||||
4. ไปที่ https://ads.google.com/ → Tools → API Center
|
||||
- ขอ Developer Token (ต้องผ่าน basic approval)
|
||||
5. ตั้งค่า environment:
|
||||
```bash
|
||||
export GOOGLE_ADS_DEVELOPER_TOKEN="your-token"
|
||||
export GOOGLE_ADS_CLIENT_ID="your-client-id"
|
||||
export GOOGLE_ADS_CLIENT_SECRET="your-client-secret"
|
||||
export GOOGLE_ADS_REFRESH_TOKEN="your-refresh-token"
|
||||
export GOOGLE_ADS_LOGIN_CUSTOMER_ID="manager-account-id"
|
||||
```
|
||||
|
||||
### ทางเลือก: Google Ads MCP Server
|
||||
https://github.com/googleads/google-ads-mcp
|
||||
ใช้กับ Hermes/Claude โดยตรง ไม่ต้องเขียน script แยก
|
||||
|
||||
## Meta Ads API
|
||||
|
||||
### Dependencies
|
||||
```bash
|
||||
pip install facebook-business
|
||||
```
|
||||
|
||||
### Authentication
|
||||
1. ไปที่ https://developers.facebook.com/ → สร้าง App
|
||||
2. เพิ่ม Marketing API product
|
||||
3. ต้องผ่าน business verification (ใช้บัตรประชาชน/เอกบริษัท)
|
||||
4. Generate access token:
|
||||
- permissions: `ads_read`, `ads_management`, `business_management`
|
||||
5. ตั้งค่า environment:
|
||||
```bash
|
||||
export META_ACCESS_TOKEN="your-token"
|
||||
export META_AD_ACCOUNT_ID="act_xxxxxxxx"
|
||||
export META_APP_ID="your-app-id"
|
||||
export META_APP_SECRET="your-app-secret"
|
||||
```
|
||||
|
||||
## Google Search Console API
|
||||
|
||||
### Dependencies
|
||||
```bash
|
||||
pip install google-auth google-auth-oauthlib google-auth-httplib2
|
||||
```
|
||||
|
||||
### Authentication
|
||||
ใช้ gcloud application-default credentials:
|
||||
```bash
|
||||
gcloud auth application-default login \
|
||||
--scopes=https://www.googleapis.com/auth/webmasters.readonly
|
||||
```
|
||||
|
||||
### หมายสำคัญ
|
||||
- **seo-analysis skill** มี scripts GSC ครบถ้วนที่ `~/.hermes/skills/seo-analysis/scripts/`
|
||||
- สามารถ adapt มาใช้กับ multi-client ได้
|
||||
- แต่ละ client ต้องเพิ่ม service account email ใน GSC property → Settings → Users
|
||||
|
||||
## แนวทางการสร้าง scripts จริง
|
||||
|
||||
เมื่อพร้อม implement scripts จริง:
|
||||
|
||||
1. **Copy pattern** จาก seo-analysis skill scripts — มีโครงสร้างครบ (analyze_gsc.py, url_inspection.py, ฯลฯ)
|
||||
2. **แยก config** ต่อ client — ใช้ .env หรือ config.json ต่อ client
|
||||
3. **Output format** — JSON ที่ orchestrator อ่านได้ (state.json friendly)
|
||||
4. **Error handling** — ถ้า token หมดอายุ หรือ API quota หมด ให้แจ้ง orchestrator
|
||||
37
scripts/google-ads.sh
Executable file
37
scripts/google-ads.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Google Ads API Bridge Script
|
||||
# =============================
|
||||
# Placeholder — implement when Google Ads API access is configured
|
||||
#
|
||||
# Required setup:
|
||||
# 1. Google Ads API developer token
|
||||
# 2. OAuth 2.0 client credentials
|
||||
# 3. Google Ads MCP server (recommended) or direct API client
|
||||
# https://github.com/googleads/google-ads-mcp
|
||||
#
|
||||
# Usage:
|
||||
# ./google-ads.sh <action> <customer-id> [options]
|
||||
#
|
||||
# Actions:
|
||||
# audit Run full Google Ads audit (uses ads-google skill data)
|
||||
# campaigns List active campaign structure
|
||||
# keywords Get keyword performance data
|
||||
# create Create new campaign from campaign-brief.md
|
||||
# stats Get performance stats for a date range
|
||||
#
|
||||
# This script is called by the moreminimore-orchestrator when
|
||||
# campaign execution is requested.
|
||||
|
||||
echo "⚠️ Google Ads API script not yet implemented."
|
||||
echo ""
|
||||
echo "Required before use:"
|
||||
echo " 1. Create OAuth 2.0 credentials in Google Cloud Console"
|
||||
echo " 2. Apply for Google Ads API developer token"
|
||||
echo " 3. Install google-ads Python client: pip install google-ads"
|
||||
echo " 4. Set up authentication: export GOOGLE_ADS_JSON_KEY=/path/to/key.json"
|
||||
echo ""
|
||||
echo "Alternative: Use Google Ads MCP server for automated data collection"
|
||||
echo " https://github.com/googleads/google-ads-mcp"
|
||||
echo ""
|
||||
echo "See: /Users/kunthawat/Gitea/moreminimore-service-system/scripts/README.md"
|
||||
exit 1
|
||||
32
scripts/meta-ads.sh
Executable file
32
scripts/meta-ads.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Meta Ads API Bridge Script
|
||||
# ===========================
|
||||
# Placeholder — implement when Meta Ads API access is configured
|
||||
#
|
||||
# Required setup:
|
||||
# 1. Facebook App with Marketing API permission
|
||||
# 2. Access token with ads_read + ads_manage scope
|
||||
# 3. Ad account ID
|
||||
#
|
||||
# Usage:
|
||||
# ./meta-ads.sh <action> <ad-account-id> [options]
|
||||
#
|
||||
# Actions:
|
||||
# audit Run full Meta Ads audit (uses ads-meta skill data)
|
||||
# campaigns List active campaign structure
|
||||
# create Create new campaign from campaign-brief.md
|
||||
# stats Get performance stats for a date range
|
||||
#
|
||||
# This script is called by the moreminimore-orchestrator when
|
||||
# campaign execution is requested.
|
||||
|
||||
echo "⚠️ Meta Ads API script not yet implemented."
|
||||
echo ""
|
||||
echo "Required before use:"
|
||||
echo " 1. Create Facebook App at https://developers.facebook.com/"
|
||||
echo " 2. Get Marketing API access (needs business verification)"
|
||||
echo " 3. Generate long-lived access token"
|
||||
echo " 4. Install facebook-business Python SDK: pip install facebook-business"
|
||||
echo ""
|
||||
echo "See: /Users/kunthawat/Gitea/moreminimore-service-system/scripts/README.md"
|
||||
exit 1
|
||||
419
scripts/mm_google_ads.py
Normal file
419
scripts/mm_google_ads.py
Normal file
@@ -0,0 +1,419 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Google Ads API for moreminimore-service.
|
||||
Reads clients-config.json, pulls campaign/keyword/ad data.
|
||||
|
||||
Usage:
|
||||
python3 mm_google_ads.py --client moreminimore campaigns
|
||||
python3 mm_google_ads.py --client moreminimore keywords
|
||||
python3 mm_google_ads.py --client moreminimore stats --days 30
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
|
||||
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_google_ads_config(config, client):
|
||||
"""Get Google Ads credentials from config"""
|
||||
google = config.get("global", {}).get("google", {})
|
||||
developer_token = google.get("developer_token", "")
|
||||
manager_account_id = google.get("manager_account_id", "")
|
||||
customer_id = client.get("analytics", {}).get("google_ads_customer_id", "")
|
||||
|
||||
if not developer_token:
|
||||
print("ERROR: No developer_token in global.google config", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not customer_id:
|
||||
print(f"ERROR: No google_ads_customer_id for client '{client['id']}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Remove hyphens from customer_id
|
||||
customer_id = customer_id.replace("-", "")
|
||||
|
||||
return {
|
||||
"developer_token": developer_token,
|
||||
"manager_account_id": manager_account_id.replace("-", ""),
|
||||
"customer_id": customer_id,
|
||||
}
|
||||
|
||||
|
||||
def get_access_token(key_path):
|
||||
"""Get access token from service account key using openssl for JWT signing"""
|
||||
import subprocess
|
||||
import base64
|
||||
import time
|
||||
|
||||
if not os.path.exists(key_path):
|
||||
print(f"ERROR: Service account key not found: {key_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(key_path) as f:
|
||||
key_data = json.load(f)
|
||||
|
||||
# JWT header
|
||||
header = {"alg": "RS256", "typ": "JWT"}
|
||||
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
|
||||
|
||||
# JWT payload
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
"iss": key_data["client_email"],
|
||||
"scope": "https://www.googleapis.com/auth/adwords",
|
||||
"aud": "https://oauth2.googleapis.com/token",
|
||||
"iat": now,
|
||||
"exp": now + 3600,
|
||||
}
|
||||
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
|
||||
|
||||
# Sign with openssl
|
||||
signing_input = f"{header_b64}.{payload_b64}"
|
||||
|
||||
# Write private key to temp file
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as kf:
|
||||
kf.write(key_data["private_key"])
|
||||
key_file = kf.name
|
||||
|
||||
try:
|
||||
# Sign with openssl
|
||||
result = subprocess.run(
|
||||
["openssl", "dgst", "-sha256", "-sign", key_file],
|
||||
input=signing_input.encode(),
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"ERROR: openssl signing failed: {result.stderr.decode()}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
signature_b64 = base64.urlsafe_b64encode(result.stdout).decode().rstrip('=')
|
||||
signed_jwt = f"{signing_input}.{signature_b64}"
|
||||
finally:
|
||||
os.unlink(key_file)
|
||||
|
||||
# Exchange JWT for access token
|
||||
token_url = "https://oauth2.googleapis.com/token"
|
||||
data = urllib.parse.urlencode({
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
"assertion": signed_jwt,
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(token_url, data=data, method="POST")
|
||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
token_data = json.loads(resp.read().decode())
|
||||
return token_data["access_token"]
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to get access token: {e}", file=sys.stderr)
|
||||
print(f" Key file: {key_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def google_ads_query(customer_id, query, developer_token, access_token, manager_account_id=None):
|
||||
"""Execute a GAQL query against Google Ads API"""
|
||||
url = f"https://googleads.googleapis.com/v24/customers/{customer_id}/googleAds:searchStream"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"developer-token": developer_token,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
if manager_account_id:
|
||||
headers["login-customer-id"] = manager_account_id
|
||||
|
||||
body = json.dumps({"query": query}).encode()
|
||||
|
||||
req = urllib.request.Request(url, data=body, headers=headers, 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: Google Ads 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(ads_config, access_token):
|
||||
"""List active campaigns"""
|
||||
query = """
|
||||
SELECT
|
||||
campaign.id,
|
||||
campaign.name,
|
||||
campaign.status,
|
||||
campaign.advertising_channel_type,
|
||||
metrics.impressions,
|
||||
metrics.clicks,
|
||||
metrics.cost_micros,
|
||||
metrics.conversions
|
||||
FROM campaign
|
||||
WHERE campaign.status = 'ENABLED'
|
||||
ORDER BY campaign.name
|
||||
"""
|
||||
|
||||
result = google_ads_query(
|
||||
ads_config["customer_id"],
|
||||
query,
|
||||
ads_config["developer_token"],
|
||||
access_token,
|
||||
ads_config.get("manager_account_id"),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def list_keywords(ads_config, access_token):
|
||||
"""List active keywords with performance"""
|
||||
query = """
|
||||
SELECT
|
||||
ad_group.name,
|
||||
ad_group_criterion.keyword.text,
|
||||
ad_group_criterion.keyword.match_type,
|
||||
ad_group_criterion.quality_info.quality_score,
|
||||
metrics.impressions,
|
||||
metrics.clicks,
|
||||
metrics.cost_micros,
|
||||
metrics.conversions,
|
||||
metrics.ctr,
|
||||
metrics.average_cpc
|
||||
FROM keyword_view
|
||||
WHERE ad_group_criterion.status = 'ENABLED'
|
||||
ORDER BY metrics.impressions DESC
|
||||
LIMIT 100
|
||||
"""
|
||||
|
||||
result = google_ads_query(
|
||||
ads_config["customer_id"],
|
||||
query,
|
||||
ads_config["developer_token"],
|
||||
access_token,
|
||||
ads_config.get("manager_account_id"),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_stats(ads_config, access_token, days=30):
|
||||
"""Get campaign performance stats"""
|
||||
query = f"""
|
||||
SELECT
|
||||
campaign.name,
|
||||
segments.date,
|
||||
metrics.impressions,
|
||||
metrics.clicks,
|
||||
metrics.cost_micros,
|
||||
metrics.conversions,
|
||||
metrics.ctr,
|
||||
metrics.average_cpc,
|
||||
metrics.cost_per_conversion
|
||||
FROM campaign
|
||||
WHERE campaign.status = 'ENABLED'
|
||||
AND segments.date DURING LAST_{days}_DAYS
|
||||
ORDER BY segments.date DESC
|
||||
"""
|
||||
|
||||
result = google_ads_query(
|
||||
ads_config["customer_id"],
|
||||
query,
|
||||
ads_config["developer_token"],
|
||||
access_token,
|
||||
ads_config.get("manager_account_id"),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def format_currency(micros):
|
||||
"""Format micros to currency"""
|
||||
if not micros:
|
||||
return "฿0"
|
||||
return f"฿{int(micros) / 1_000_000:,.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 in a table"""
|
||||
if not data or not data[0].get("results"):
|
||||
print("ไม่พบ campaign ที่ active")
|
||||
return
|
||||
|
||||
results = data[0]["results"]
|
||||
print(f"\n📋 Active Campaigns ({len(results)})")
|
||||
print("-" * 100)
|
||||
print(f"{'Name':<30} {'Type':<15} {'Impressions':>12} {'Clicks':>10} {'Cost':>12} {'Conv':>8}")
|
||||
print("-" * 100)
|
||||
|
||||
for row in results:
|
||||
campaign = row.get("campaign", {})
|
||||
metrics = row.get("metrics", {})
|
||||
name = campaign.get("name", "")[:28]
|
||||
channel = campaign.get("advertisingChannelType", "")
|
||||
print(f"{name:<30} {channel:<15} {format_number(metrics.get('impressions', 0)):>12} "
|
||||
f"{format_number(metrics.get('clicks', 0)):>10} "
|
||||
f"{format_currency(metrics.get('costMicros', 0)):>12} "
|
||||
f"{format_number(metrics.get('conversions', 0)):>8}")
|
||||
|
||||
|
||||
def display_keywords(data):
|
||||
"""Display keyword data in a table"""
|
||||
if not data or not data[0].get("results"):
|
||||
print("ไม่พบ keyword ที่ active")
|
||||
return
|
||||
|
||||
results = data[0]["results"]
|
||||
print(f"\n🔑 Keywords ({len(results)})")
|
||||
print("-" * 110)
|
||||
print(f"{'Keyword':<35} {'Match':<10} {'QS':>4} {'Impressions':>12} {'Clicks':>10} {'CTR':>8} {'CPC':>10}")
|
||||
print("-" * 110)
|
||||
|
||||
for row in results:
|
||||
criterion = row.get("adGroupCriterion", {})
|
||||
keyword = criterion.get("keyword", {})
|
||||
metrics = row.get("metrics", {})
|
||||
qs = criterion.get("qualityInfo", {}).get("qualityScore", "")
|
||||
|
||||
kw_text = keyword.get("text", "")[:33]
|
||||
match_type = keyword.get("matchType", "")
|
||||
ctr = float(metrics.get("ctr", 0)) * 100
|
||||
avg_cpc = int(metrics.get("averageCpc", 0)) / 1_000_000
|
||||
|
||||
print(f"{kw_text:<35} {match_type:<10} {qs:>4} "
|
||||
f"{format_number(metrics.get('impressions', 0)):>12} "
|
||||
f"{format_number(metrics.get('clicks', 0)):>10} "
|
||||
f"{ctr:>7.2f}% "
|
||||
f"฿{avg_cpc:>8.2f}")
|
||||
|
||||
|
||||
def display_stats(data):
|
||||
"""Display stats in a table"""
|
||||
if not data or not data[0].get("results"):
|
||||
print("ไม่พบข้อมูลสถิติ")
|
||||
return
|
||||
|
||||
results = data[0]["results"]
|
||||
|
||||
# Aggregate by campaign
|
||||
campaigns = {}
|
||||
for row in results:
|
||||
campaign_name = row.get("campaign", {}).get("name", "")
|
||||
metrics = row.get("metrics", {})
|
||||
|
||||
if campaign_name not in campaigns:
|
||||
campaigns[campaign_name] = {
|
||||
"impressions": 0, "clicks": 0, "cost": 0, "conversions": 0
|
||||
}
|
||||
|
||||
campaigns[campaign_name]["impressions"] += int(metrics.get("impressions", 0))
|
||||
campaigns[campaign_name]["clicks"] += int(metrics.get("clicks", 0))
|
||||
campaigns[campaign_name]["cost"] += int(metrics.get("costMicros", 0))
|
||||
campaigns[campaign_name]["conversions"] += float(metrics.get("conversions", 0))
|
||||
|
||||
print(f"\n📊 Campaign Stats")
|
||||
print("-" * 100)
|
||||
print(f"{'Campaign':<30} {'Impressions':>12} {'Clicks':>10} {'Cost':>12} {'Conv':>8} {'CTR':>8} {'CPA':>12}")
|
||||
print("-" * 100)
|
||||
|
||||
for name, m in campaigns.items():
|
||||
ctr = (m["clicks"] / m["impressions"] * 100) if m["impressions"] > 0 else 0
|
||||
cpa = (m["cost"] / m["conversions"]) if m["conversions"] > 0 else 0
|
||||
|
||||
print(f"{name[:28]:<30} {format_number(m['impressions']):>12} "
|
||||
f"{format_number(m['clicks']):>10} "
|
||||
f"{format_currency(m['cost']):>12} "
|
||||
f"{int(m['conversions']):>8} "
|
||||
f"{ctr:>7.2f}% "
|
||||
f"{format_currency(cpa):>12}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Google Ads API for moreminimore-service")
|
||||
parser.add_argument("--client", required=True, help="Client ID from clients-config.json")
|
||||
parser.add_argument("action", choices=["campaigns", "keywords", "stats"], help="Action to perform")
|
||||
parser.add_argument("--days", type=int, default=30, help="Number of days for stats (default: 30)")
|
||||
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 Google Ads config
|
||||
ads_config = get_google_ads_config(config, client)
|
||||
|
||||
print(f"📊 Google Ads — {client['display_name']}")
|
||||
print(f" Customer ID: {ads_config['customer_id']}")
|
||||
print()
|
||||
|
||||
# Get access token from service account key
|
||||
key_path = config.get("global", {}).get("google", {}).get("service_account_key", "")
|
||||
if not key_path:
|
||||
print("ERROR: No service_account_key in global.google config", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
access_token = get_access_token(os.path.expanduser(key_path))
|
||||
|
||||
# Execute action
|
||||
if args.action == "campaigns":
|
||||
data = list_campaigns(ads_config, access_token)
|
||||
if args.json:
|
||||
print(json.dumps(data, indent=2))
|
||||
else:
|
||||
display_campaigns(data)
|
||||
|
||||
elif args.action == "keywords":
|
||||
data = list_keywords(ads_config, access_token)
|
||||
if args.json:
|
||||
print(json.dumps(data, indent=2))
|
||||
else:
|
||||
display_keywords(data)
|
||||
|
||||
elif args.action == "stats":
|
||||
data = get_stats(ads_config, access_token, args.days)
|
||||
if args.json:
|
||||
print(json.dumps(data, indent=2))
|
||||
else:
|
||||
display_stats(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
319
scripts/mm_gsc.py
Normal file
319
scripts/mm_gsc.py
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GSC Analytics for moreminimore-service.
|
||||
Reads clients-config.json, pulls GSC data using service account key.
|
||||
|
||||
Usage:
|
||||
python3 mm_gsc.py --client moreminimore --days 90
|
||||
python3 mm_gsc.py --client moreminimore --days 28
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
def load_config():
|
||||
"""Load clients-config.json"""
|
||||
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
|
||||
if not os.path.exists(config_path):
|
||||
print(f"ERROR: Config not found at {config_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
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_gsc_property(client):
|
||||
"""Get GSC property from client config"""
|
||||
analytics = client.get("analytics", {})
|
||||
gsc_property = analytics.get("gsc_property", "")
|
||||
if not gsc_property:
|
||||
print(f"ERROR: No gsc_property set for client '{client['id']}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return gsc_property
|
||||
|
||||
|
||||
def get_access_token(key_path):
|
||||
"""Get access token from service account key using openssl for JWT signing"""
|
||||
import subprocess
|
||||
import base64
|
||||
import time
|
||||
|
||||
if not os.path.exists(key_path):
|
||||
print(f"ERROR: Service account key not found: {key_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(key_path) as f:
|
||||
key_data = json.load(f)
|
||||
|
||||
# JWT header
|
||||
header = {"alg": "RS256", "typ": "JWT"}
|
||||
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
|
||||
|
||||
# JWT payload
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
"iss": key_data["client_email"],
|
||||
"scope": "https://www.googleapis.com/auth/webmasters.readonly",
|
||||
"aud": "https://oauth2.googleapis.com/token",
|
||||
"iat": now,
|
||||
"exp": now + 3600,
|
||||
}
|
||||
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
|
||||
|
||||
# Sign with openssl
|
||||
signing_input = f"{header_b64}.{payload_b64}"
|
||||
|
||||
# Write private key to temp file
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as kf:
|
||||
kf.write(key_data["private_key"])
|
||||
key_file = kf.name
|
||||
|
||||
try:
|
||||
# Sign with openssl
|
||||
result = subprocess.run(
|
||||
["openssl", "dgst", "-sha256", "-sign", key_file],
|
||||
input=signing_input.encode(),
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"ERROR: openssl signing failed: {result.stderr.decode()}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
signature_b64 = base64.urlsafe_b64encode(result.stdout).decode().rstrip('=')
|
||||
signed_jwt = f"{signing_input}.{signature_b64}"
|
||||
finally:
|
||||
os.unlink(key_file)
|
||||
|
||||
# Exchange JWT for access token
|
||||
token_url = "https://oauth2.googleapis.com/token"
|
||||
data = urllib.parse.urlencode({
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
"assertion": signed_jwt,
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(token_url, data=data, method="POST")
|
||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
token_data = json.loads(resp.read().decode())
|
||||
return token_data["access_token"]
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to get access token: {e}", file=sys.stderr)
|
||||
print(f" Key file: {key_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
def gsc_query(site_property, access_token, request_body):
|
||||
"""Execute a GSC API query"""
|
||||
# URL encode the site property
|
||||
import urllib.parse
|
||||
site_encoded = urllib.parse.quote(site_property, safe='')
|
||||
|
||||
url = f"https://searchconsole.googleapis.com/webmasters/v3/sites/{site_encoded}/searchAnalytics/query"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
body = json.dumps(request_body).encode()
|
||||
req = urllib.request.Request(url, data=body, headers=headers, 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: GSC 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 get_top_queries(site_property, access_token, days=28):
|
||||
"""Get top queries"""
|
||||
end_date = date.today() - timedelta(days=3) # GSC data lags ~3 days
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
result = gsc_query(site_property, access_token, {
|
||||
"startDate": start_date.isoformat(),
|
||||
"endDate": end_date.isoformat(),
|
||||
"dimensions": ["query"],
|
||||
"rowLimit": 50,
|
||||
"type": "web",
|
||||
})
|
||||
|
||||
return result.get("rows", [])
|
||||
|
||||
|
||||
def get_top_pages(site_property, access_token, days=28):
|
||||
"""Get top pages"""
|
||||
end_date = date.today() - timedelta(days=3)
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
result = gsc_query(site_property, access_token, {
|
||||
"startDate": start_date.isoformat(),
|
||||
"endDate": end_date.isoformat(),
|
||||
"dimensions": ["page"],
|
||||
"rowLimit": 50,
|
||||
"type": "web",
|
||||
})
|
||||
|
||||
return result.get("rows", [])
|
||||
|
||||
|
||||
def get_summary(site_property, access_token, days=28):
|
||||
"""Get overall summary"""
|
||||
end_date = date.today() - timedelta(days=3)
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
result = gsc_query(site_property, access_token, {
|
||||
"startDate": start_date.isoformat(),
|
||||
"endDate": end_date.isoformat(),
|
||||
"dimensions": ["date"],
|
||||
"type": "web",
|
||||
})
|
||||
|
||||
rows = result.get("rows", [])
|
||||
total_clicks = sum(r.get("clicks", 0) for r in rows)
|
||||
total_impressions = sum(r.get("impressions", 0) for r in rows)
|
||||
avg_ctr = (total_clicks / total_impressions * 100) if total_impressions > 0 else 0
|
||||
avg_position = sum(r.get("position", 0) * r.get("impressions", 0) for r in rows) / total_impressions if total_impressions > 0 else 0
|
||||
|
||||
return {
|
||||
"clicks": total_clicks,
|
||||
"impressions": total_impressions,
|
||||
"ctr": avg_ctr,
|
||||
"position": avg_position,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def display_summary(summary):
|
||||
"""Display summary"""
|
||||
print(f"📊 Summary ({summary['start_date']} to {summary['end_date']})")
|
||||
print("-" * 60)
|
||||
print(f" Clicks: {summary['clicks']:,}")
|
||||
print(f" Impressions: {summary['impressions']:,}")
|
||||
print(f" CTR: {summary['ctr']:.2f}%")
|
||||
print(f" Avg Position: {summary['position']:.1f}")
|
||||
|
||||
|
||||
def display_queries(rows):
|
||||
"""Display top queries"""
|
||||
if not rows:
|
||||
print("\nไม่พบ queries")
|
||||
return
|
||||
|
||||
print(f"\n🔑 Top Queries ({len(rows)})")
|
||||
print("-" * 90)
|
||||
print(f"{'Query':<45} {'Clicks':>10} {'Impressions':>12} {'CTR':>8} {'Position':>10}")
|
||||
print("-" * 90)
|
||||
|
||||
for row in rows[:30]:
|
||||
keys = row.get("keys", [])
|
||||
query = keys[0] if keys else ""
|
||||
ctr = row.get("ctr", 0) * 100
|
||||
|
||||
print(f"{query[:43]:<45} {row.get('clicks', 0):>10,} "
|
||||
f"{row.get('impressions', 0):>12,} "
|
||||
f"{ctr:>7.2f}% "
|
||||
f"{row.get('position', 0):>10.1f}")
|
||||
|
||||
|
||||
def display_pages(rows):
|
||||
"""Display top pages"""
|
||||
if not rows:
|
||||
print("\nไม่พบ pages")
|
||||
return
|
||||
|
||||
print(f"\n📄 Top Pages ({len(rows)})")
|
||||
print("-" * 90)
|
||||
print(f"{'Page':<55} {'Clicks':>10} {'Impressions':>12} {'CTR':>8}")
|
||||
print("-" * 90)
|
||||
|
||||
for row in rows[:20]:
|
||||
keys = row.get("keys", [])
|
||||
page = keys[0] if keys else ""
|
||||
ctr = row.get("ctr", 0) * 100
|
||||
|
||||
print(f"{page[:53]:<55} {row.get('clicks', 0):>10,} "
|
||||
f"{row.get('impressions', 0):>12,} "
|
||||
f"{ctr:>7.2f}%")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="GSC Analytics for moreminimore-service")
|
||||
parser.add_argument("--client", required=True, help="Client ID from clients-config.json")
|
||||
parser.add_argument("--days", type=int, default=90, help="Number of days to analyze (default: 90)")
|
||||
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 in config", file=sys.stderr)
|
||||
print(f"Available clients: {[c['id'] for c in config.get('clients', [])]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get GSC property
|
||||
gsc_property = get_gsc_property(client)
|
||||
|
||||
# Get service account key
|
||||
key_path = config.get("global", {}).get("google", {}).get("service_account_key", "")
|
||||
if not key_path:
|
||||
print("ERROR: No service_account_key in global.google config", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"📊 GSC Analytics for: {client['display_name']}")
|
||||
print(f" Property: {gsc_property}")
|
||||
print(f" Period: {args.days} days")
|
||||
print()
|
||||
|
||||
# Get access token
|
||||
access_token = get_access_token(os.path.expanduser(key_path))
|
||||
|
||||
# Get data
|
||||
summary = get_summary(gsc_property, access_token, args.days)
|
||||
queries = get_top_queries(gsc_property, access_token, args.days)
|
||||
pages = get_top_pages(gsc_property, access_token, args.days)
|
||||
|
||||
if args.json:
|
||||
result = {
|
||||
"summary": summary,
|
||||
"queries": queries,
|
||||
"pages": pages,
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
display_summary(summary)
|
||||
display_queries(queries)
|
||||
display_pages(pages)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
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()
|
||||
38
scripts/search-console.sh
Executable file
38
scripts/search-console.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Google Search Console Bridge Script
|
||||
# ====================================
|
||||
# Placeholder — implement when GSC API access is configured
|
||||
#
|
||||
# Required setup:
|
||||
# 1. Google Cloud Project with Search Console API enabled
|
||||
# 2. GCP service account (or OAuth) with access to client's GSC property
|
||||
# 3. Client must add service account email as GSC property user
|
||||
#
|
||||
# Usage:
|
||||
# ./search-console.sh <action> <site-url> [options]
|
||||
#
|
||||
# Actions:
|
||||
# top-queries Get top queries by clicks/impressions
|
||||
# top-pages Get top pages by clicks/impressions
|
||||
# trends Get traffic trends over time
|
||||
# content-gaps Find queries ranking 11-30 with no dedicated page
|
||||
# ctr-gaps Find high-impression, low-CTR opportunities
|
||||
#
|
||||
# This script is called by the moreminimore-orchestrator when
|
||||
# analytics monitoring is active (Phase 3 — Analytics Loop).
|
||||
|
||||
echo "⚠️ Google Search Console API script not yet implemented."
|
||||
echo ""
|
||||
echo "Required before use:"
|
||||
echo " 1. Create Google Cloud Project"
|
||||
echo " 2. Enable Search Console API: gcloud services enable searchconsole.googleapis.com"
|
||||
echo " 3. Set up service account or application-default credentials"
|
||||
echo " 4. Client adds service account email to GSC property as user"
|
||||
echo ""
|
||||
echo "The seo-analysis skill has full GSC scripts at:"
|
||||
echo " ~/.hermes/skills/seo-analysis/scripts/"
|
||||
echo ""
|
||||
echo "Consider adapting those scripts for multi-client use."
|
||||
echo ""
|
||||
echo "See: /Users/kunthawat/Gitea/moreminimore-service-system/scripts/README.md"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user