← back to Marketing Command Center

scripts/li-cma-watch.py

139 lines

#!/usr/bin/env python3
"""
li-cma-watch.py — unattended watcher for LinkedIn's Community Management API
(CMA) access-request DECISION on the DW app 259400006.

The CMA request for Designer Wallcoverings sits in LinkedIn's review queue
(stage 1 of 2, ~10-14 business days). LinkedIn notifies by email to the app's
verified business email = info@designerwallcoverings.com. This poller checks
that inbox (read-only, via George's local HTTP API — no token cost, no send
gate touched) and, when a *decision* email arrives, loudly surfaces it to Steve
with the exact next step.

Runs from a launchd LaunchAgent 3x/day. Alerts only on NEW, decision-looking
mail — routine "Verify your business email" / device notices are filtered out,
and each message id is remembered so it never double-alerts.

Next step on approval (told to Steve in the alert):
  python3 ~/.claude/skills/linkedin-api/scripts/connect.py --scope org
  (secrets + redirect already staged 2026-08-12) -> route token+URN into MCC ->
  ssh root@45.61.58.125 'pm2 reload marketing-command-center --update-env'
"""
import json, os, re, subprocess, sys, time, urllib.parse, urllib.request

HERE      = os.path.dirname(os.path.abspath(__file__))
SEEN_PATH = os.path.join(HERE, ".li-cma-watch-seen.json")
LOG_PATH  = os.path.join(HERE, "li-cma-watch.log")
DESKTOP_MARKER = os.path.expanduser("~/Desktop/LINKEDIN-CMA-RESPONDED.txt")
GEORGE    = "http://127.0.0.1:9850"
ACCOUNT   = "info"          # info@designerwallcoverings.com — the app's business email
SEARCH_Q  = "from:linkedin newer_than:25d"

# A message is a DECISION worth alerting on if it matches a signal AND is NOT
# one of the known routine notices.
SIGNAL = re.compile(r"(access request|community management|marketing developer|"
                    r"approved|granted|declined|rejected|denied|unable to|"
                    r"additional (information|documentation)|your request|"
                    r"api (access|product))", re.I)
NOISE  = re.compile(r"(verify your business email|verify your new device|"
                    r"new device|sign-?in|password|security code|weekly|"
                    r"who'?s viewed|notification digest|invitation)", re.I)


def log(msg):
    line = time.strftime("%Y-%m-%d %H:%M:%S") + "  " + msg
    try:
        with open(LOG_PATH, "a") as f:
            f.write(line + "\n")
    except Exception:
        pass
    print(line, flush=True)


def george_auth():
    p = os.path.expanduser("~/.claude.json")
    d = json.load(open(p))
    return d.get("mcpServers", {}).get("george", {}).get("env", {}).get("GEORGE_BASIC_AUTH", "")


def search():
    auth = george_auth()
    if not auth:
        log("ERR no GEORGE_BASIC_AUTH in ~/.claude.json"); return []
    url = (GEORGE + "/api/search?account=" + ACCOUNT + "&maxResults=15&q="
           + urllib.parse.quote(SEARCH_Q))
    req = urllib.request.Request(url, headers={"Authorization": "Basic " + auth})
    try:
        raw = urllib.request.urlopen(req, timeout=12).read().decode()
    except Exception as e:
        log("ERR George search failed: %s" % e); return []
    d = json.loads(raw)
    return d if isinstance(d, list) else d.get("messages", d.get("results", []))


def load_seen():
    try:    return set(json.load(open(SEEN_PATH)))
    except Exception: return set()


def save_seen(s):
    try:    json.dump(sorted(s), open(SEEN_PATH, "w"))
    except Exception as e: log("ERR save seen: %s" % e)


def alert(hits):
    subj = hits[0].get("subject", "(no subject)")
    body = (
        "LinkedIn responded about the DW Community Management API (CMA) access "
        "request.\n\n"
        + "\n".join("- %s | %s" % (h.get("date", "?")[:16], h.get("subject", "?"))
                    for h in hits)
        + "\n\nIf APPROVED, the go-live is one command (secrets + redirect already "
          "staged):\n"
          "  python3 ~/.claude/skills/linkedin-api/scripts/connect.py --scope org\n"
          "  -> route token+URN into MCC -> pm2 reload marketing-command-center\n\n"
          "If they ask for DOCS, the request is still open — reply from info@.\n"
          "Check info@ for the full message.\n"
    )
    # 1) Desktop marker (impossible to miss, durable)
    try:
        with open(DESKTOP_MARKER, "w") as f:
            f.write(time.strftime("%Y-%m-%d %H:%M:%S") + "\n\n" + body)
    except Exception as e:
        log("ERR desktop marker: %s" % e)
    # 2) macOS notification with sound
    try:
        subprocess.run(["osascript", "-e",
            'display notification "%s" with title "LinkedIn CMA — LinkedIn responded" '
            'subtitle "Check info@ / Desktop marker" sound name "Glass"'
            % subj.replace('"', "'")[:200]], timeout=8)
    except Exception as e:
        log("ERR osascript: %s" % e)
    # 3) log
    log("ALERT — LinkedIn CMA decision mail detected: " + subj)


def main():
    seen = load_seen()
    msgs = search()
    hits = []
    for m in msgs:
        mid  = m.get("id") or m.get("messageId") or ""
        text = (m.get("subject", "") + " " + m.get("snippet", ""))
        if not mid:
            continue
        if SIGNAL.search(text) and not NOISE.search(text):
            if mid not in seen:
                hits.append(m)
        seen.add(mid)          # remember every LinkedIn msg id so noise never re-alerts
    if hits:
        alert(hits)
    else:
        log("heartbeat — %d LinkedIn msgs scanned, no new CMA decision" % len(msgs))
    save_seen(seen)
    return 0 if not hits else 10


if __name__ == "__main__":
    sys.exit(main())