#!/usr/bin/env python3 """ FalahOS Sales Automation — Runs every 6 hours via cron. Executes all MCP-driven tasks in a single Python process per MCP to avoid session expiry issues. Usage: python scripts/automation.py python scripts/automation.py --mautic-only python scripts/automation.py --gumroad-only python scripts/automation.py --ghost-only """ import os import sys import json import csv import time import requests from datetime import datetime from urllib.parse import urljoin # ─── Config ────────────────────────────────────────────────── BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MAUTIC_BASE = os.environ.get("MAUTIC_BASE_URL", "https://mautic.falahos.my").rstrip("/") GUMROAD_TOKEN = os.environ.get("GUMROAD_ACCESS_TOKEN", "") GHOST_KEY = os.environ.get("GHOST_ADMIN_API_KEY", "") METRICS_FILE = os.path.join(BASE_DIR, "daily_metrics.jsonl") QUEUE_FILE = os.path.join(BASE_DIR, "content_queue.json") LEADS_FILE = os.path.join(BASE_DIR, "quality_leads.json") # ─── Helpers ───────────────────────────────────────────────── def log(msg): print(f"[{datetime.now().isoformat()}] {msg}") def append_metrics(**kwargs): kwargs["timestamp"] = time.time() kwargs["date"] = datetime.now().isoformat()[:10] with open(METRICS_FILE, "a") as f: f.write(json.dumps(kwargs) + "\n") # ─── 1. MAUTIC: Broadcast + Nurture ───────────────────────── def run_mautic(): """ All Mautic operations in a single process to avoid session expiry. Uses web session login (not OAuth2 API). """ log("Mautic: starting...") # Import server module in same process sys.path.insert(0, "/Users/wmj24/.hermes/mcp-servers/mautic-server") from server import _web_login, _session, BASE, API_TIMEOUT, _ensure_auth import re from bs4 import BeautifulSoup # Login result = _web_login() if not result.get("success"): log(f"Mautic: login failed — {result.get('error', 'unknown')}") return False log("Mautic: logged in") session = requests.Session() if _session.get("cookies"): session.cookies.update(_session["cookies"]) # --- Get segments --- r = session.get(urljoin(BASE, "/s/segments"), timeout=API_TIMEOUT, verify=False) # Parse segment IDs from page segments = {} m = re.findall(r'/s/segments/(\d+)[^"]*">([^<]+)<', r.text) for seg_id, name in m: segments[name.strip()] = seg_id log(f"Mautic: found {len(segments)} segments") # --- Get contacts in high-value segment --- segment_id = None for name, sid in segments.items(): if "high-value" in name.lower() or "islamic" in name.lower(): segment_id = sid break if segment_id: # Check segment contact count r = session.get(urljoin(BASE, f"/s/segments/{segment_id}/contacts"), timeout=API_TIMEOUT, verify=False) # Count contacts from page contact_count = r.text.count("contact-row") if "contact-row" in r.text else 0 log(f"Mautic: segment has ~{contact_count} contacts") # --- Check Mautic stats --- auth_result = _ensure_auth() if auth_result.get("success"): # Try API stats r = session.get(urljoin(BASE, "/s/dashboard"), timeout=API_TIMEOUT, verify=False) log("Mautic: dashboard checked") # --- Send broadcast to high-value segment --- # Rotate through email templates based on day of week day_of_week = datetime.now().weekday() email_templates = [ {"name": "Day0-Welcome", "subject": "Islamic Dev Toolkit 🕌"}, {"name": "Day2-CaseStudy", "subject": "How Gading built quran-api"}, {"name": "Day5-Tripwire", "subject": "Islamic UI Kit for $10"}, {"name": "Day8-Objections", "subject": "I'm not a designer — neither were they"}, {"name": "Day11-Core", "subject": "Complete FalahOS Stack $49"}, {"name": "Day14-FAQ", "subject": "Quick questions?"}, {"name": "Day18-Enterprise", "subject": "Building for an organization?"}, ] template = email_templates[day_of_week % len(email_templates)] log(f"Mautic: would send '{template['name']}' — '{template['subject']}'") # Note: Actual email send requires segment_id + email_id # These are created once in mautic_setup.py and referenced here log("Mautic: broadcast logic ready (needs segment_id + email_id from setup)") return True # ─── 2. GUMROAD: Cart Recovery + Revenue Check ───────────── def run_gumroad(): """Check abandoned carts and recover them. Also check daily revenue.""" log("Gumroad: starting...") if not GUMROAD_TOKEN: log("Gumroad: no token set, skipping") return False headers = {"Authorization": f"Bearer {GUMROAD_TOKEN}"} # --- Check abandoned carts --- try: r = requests.get("https://api.gumroad.com/v2/abandoned_carts", headers=headers, timeout=20) if r.status_code == 200: carts = r.json().get("abandoned_carts", []) recovered = 0 for cart in carts: if not cart.get("recovery_email_sent"): try: r2 = requests.post( f"https://api.gumroad.com/v2/abandoned_carts/{cart['id']}/send_recovery_email", headers=headers, timeout=20 ) if r2.status_code == 200: recovered += 1 log(f" Recovered cart: {cart['id']} (${cart.get('amount', 0)/100:.2f})") except Exception as e: log(f" Cart recovery error: {e}") log(f"Gumroad: recovered {recovered} carts") else: log(f"Gumroad: cart check returned {r.status_code}") except Exception as e: log(f"Gumroad: cart check error — {e}") # --- Check daily revenue --- try: r = requests.get("https://api.gumroad.com/v2/reports/overview", headers=headers, timeout=20) if r.status_code == 200: data = r.json() revenue = data.get("total_revenue", 0) orders = data.get("total_sales", 0) log(f"Gumroad: revenue=${revenue/100:.2f}, orders={orders}") append_metrics(gumroad_revenue=revenue, gumroad_orders=orders) else: log(f"Gumroad: revenue check returned {r.status_code}") except Exception as e: log(f"Gumroad: revenue check error — {e}") return True # ─── 3. GHOST: Publish Next Blog Post ────────────────────── def run_ghost(): """Publish the next post from content_queue.json.""" log("Ghost: starting...") if not GHOST_KEY: log("Ghost: no API key set, skipping") return False if not os.path.exists(QUEUE_FILE): log("Ghost: no content queue found") return False with open(QUEUE_FILE) as f: queue = json.load(f) if not queue: log("Ghost: queue is empty") return False key_id, secret = GHOST_KEY.split(":", 1) import jwt token = jwt.encode( {"iat": int(time.time()), "exp": int(time.time()) + 300, "aud": "/v4/admin/"}, bytes.fromhex(secret), algorithm="HS256", headers={"kid": key_id} ) headers = { "Authorization": f"Ghost {token}", "Content-Type": "application/json", } post = queue.pop(0) data = { "posts": [{ "title": post["title"], "html": post["html"], "status": "published", "tags": post.get("tags", []), "slug": post.get("slug", ""), }] } try: r = requests.post( "https://ghost.falahos.my/ghost/api/admin/posts/", headers=headers, json=data, timeout=20 ) if r.status_code == 201: log(f"Ghost: published '{post['title']}'") else: log(f"Ghost: publish failed ({r.status_code}) — {r.text[:200]}") # Put it back if it failed queue.insert(0, post) except Exception as e: log(f"Ghost: publish error — {e}") queue.insert(0, post) with open(QUEUE_FILE, "w") as f: json.dump(queue, f, indent=2) return True # ─── 4. ANALYTICS: Snapshot ───────────────────────────────── def run_analytics(): """Capture daily metrics snapshot.""" log("Analytics: capturing snapshot...") metrics = {} # Try Gumroad revenue if GUMROAD_TOKEN: try: headers = {"Authorization": f"Bearer {GUMROAD_TOKEN}"} r = requests.get("https://api.gumroad.com/v2/reports/overview", headers=headers, timeout=10) if r.status_code == 200: data = r.json() metrics["gumroad_revenue"] = data.get("total_revenue", 0) metrics["gumroad_orders"] = data.get("total_sales", 0) except Exception: pass append_metrics(**metrics) log(f"Analytics: {json.dumps(metrics)}") return True # ─── 5. SCOUT: Weekly Lead Scrape ─────────────────────────── def run_scout_scrape(): """Scrape 10 new GitHub leads for Islamic tech. Run on Sundays only.""" log("Scout: weekly lead scrape...") sys.path.insert(0, "/Users/wmj24/.hermes/mcp-servers/scout-server/src") from app.scrapers.github import scrape_profile from app.scrapers.enrichment import enrich_lead # Search for Islamic tech contributors search_queries = [ "quran api language:javascript stars:>50", "prayer times language:javascript stars:>30", "islamic language:typescript stars:>20", "halal language:javascript stars:>20", "muslim app language:javascript stars:>50", ] new_leads = [] for query in search_queries: try: r = requests.get( "https://api.github.com/search/repositories", params={"q": query, "per_page": "10", "sort": "stars"}, headers={"Accept": "application/vnd.github.v3+json"}, timeout=15 ) if r.status_code == 200: repos = r.json().get("items", []) for repo in repos: owner = repo["owner"]["login"] # Skip already known leads known = [l.get("username") for l in _load_leads()] if owner in known: continue try: profile = scrape_profile(owner) # Only keep if it looks relevant bio = (profile.get("bio") or "").lower() name = (profile.get("full_name") or "").lower() if any(kw in bio + name for kw in ["muslim", "islam", "quran", "prayer", "halal", "arabic"]): enriched = enrich_lead(profile) new_leads.append(enriched) log(f" New lead: {owner} (score: {enriched.get('lead_score', 0)})") except Exception as e: log(f" Error scraping {owner}: {e}") except Exception as e: log(f" Search error for '{query}': {e}") # Save new leads if new_leads: existing = _load_leads() existing.extend(new_leads) with open(LEADS_FILE, "w") as f: json.dump(existing, f, indent=2) _export_csv(existing) log(f"Scout: added {len(new_leads)} new leads (total: {len(existing)})") else: log("Scout: no new leads found this week") return True def _load_leads(): if os.path.exists(LEADS_FILE): with open(LEADS_FILE) as f: return json.load(f) return [] def _export_csv(leads): csv_path = os.path.join(BASE_DIR, "quality_leads.csv") with open(csv_path, "w", newline="") as f: fieldnames = ["email", "firstname", "lastname", "company", "position", "tags", "source", "profile_url", "website", "lead_score", "notes"] writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() for lead in leads: row = lead.copy() row["tags"] = "; ".join(row.get("tags", [])) writer.writerow({k: row.get(k, "") for k in fieldnames}) # ─── MAIN ──────────────────────────────────────────────────── def main(): log("=" * 50) log("FalahOS Sales Automation Starting") log("=" * 50) # Always run these run_mautic() run_gumroad() run_ghost() run_analytics() # Weekly (Sunday = 6) if datetime.now().weekday() == 6: run_scout_scrape() log("=" * 50) log("FalahOS Automation Complete") log("=" * 50) if __name__ == "__main__": main()