← back to Zendesk Chat Analyzer

recent.py

75 lines

#!/usr/bin/env python3
"""Fetch RECENT chats (last N hours) from Zendesk Chat and print JSON to stdout.
Used by the server's /api/recent for the Live feed. Token stays server-side."""
import json, os, re, sys, time, urllib.request, urllib.parse

ENV = os.path.expanduser("~/Projects/secrets-manager/.env")
HOURS = max(1.0, min(168.0, float(os.environ.get("HOURS", "8"))))  # clamp 1h..7d (abuse guard)
BASE = "https://www.zopim.com/api/v2/incremental/chats"


def token():
    if os.environ.get("ZENDESK_CHAT_ACCESS_TOKEN"):
        return os.environ["ZENDESK_CHAT_ACCESS_TOKEN"].strip()
    try:
        for line in open(ENV):
            if line.startswith("ZENDESK_CHAT_ACCESS_TOKEN="):
                return line.split("=", 1)[1].strip().strip('"').strip("'")
    except Exception:
        pass
    raise SystemExit("no token")


def clean_title(t):
    if not t:
        return ""
    return re.sub(r"\s+", " ", re.split(r"[–\-|]\s*Designer Wallcoverings", t)[0]).strip()


def main():
    tok = token()
    start = int(time.time() - HOURS * 3600)
    url = BASE + "?" + urllib.parse.urlencode({"fields": "chats(*)", "start_time": start})
    rows, pages = [], 0
    while url and pages < 20:
        req = urllib.request.Request(url, headers={"Authorization": "Bearer " + tok})
        try:
            d = json.load(urllib.request.urlopen(req, timeout=30))
        except Exception as e:
            print(json.dumps({"error": str(e), "chats": []})); return
        pages += 1
        for c in d.get("chats", []):
            s = c.get("session") or {}
            hist = c.get("history") or []
            vmsgs = [h.get("msg", "") for h in hist
                     if isinstance(h, dict) and str(h.get("sender_type", "")).lower() == "visitor" and h.get("msg")]
            if not vmsgs and c.get("comment"):
                vmsgs = [c.get("comment")]
            rows.append({
                "id": c.get("id"), "ts": c.get("timestamp"), "type": c.get("type"),
                "country": s.get("country_name") or "Unknown", "country_code": s.get("country_code") or "??",
                "city": s.get("city") or "", "region": s.get("region") or "", "platform": s.get("platform") or "",
                "agents": c.get("agent_names") or [], "missed": bool(c.get("missed")),
                "rating": c.get("rating") or "", "msg": " · ".join(vmsgs)[:200],
                "browsed": [clean_title(w.get("title")) for w in (c.get("webpath") or []) if w.get("title")][:1],
            })
        end = d.get("end_time"); nxt = d.get("next_page")
        if not d.get("chats") or not end:
            break
        url = nxt if nxt else None
    # keep only chats that actually STARTED within the window (incremental API also returns recently-updated old chats)
    import calendar
    cutoff = time.time() - HOURS * 3600
    def started(r):
        try:
            return calendar.timegm(time.strptime(r["ts"], "%Y-%m-%dT%H:%M:%SZ")) >= cutoff
        except Exception:
            return False
    rows = [r for r in rows if started(r)]
    rows.sort(key=lambda r: r.get("ts") or "", reverse=True)
    print(json.dumps({"now": int(time.time()), "hours": HOURS, "count": len(rows), "chats": rows[:60]}))


if __name__ == "__main__":
    main()