← 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())