#!/usr/bin/env python3 """Reprocessa eventos Chatwoot inbound que ficaram pendentes em raw_events. Uso seguro: PYTHONPATH=. python scripts/reprocess_pending_chatwoot_raw_events.py --dry-run PYTHONPATH=. python scripts/reprocess_pending_chatwoot_raw_events.py --limit 50 PYTHONPATH=. python scripts/reprocess_pending_chatwoot_raw_events.py --include-errors --limit 20 Critério principal v4928.1.5.25: source_system='chatwoot' + event_type='message_created' + payload.message_type='incoming' Não usa sender.type=contact porque em emails Chatwoot esse campo pode vir vazio. """ from __future__ import annotations import argparse import asyncio import json import os import sys from pathlib import Path from typing import Any PROJECT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PROJECT_ROOT)) os.chdir(PROJECT_ROOT) from sqlalchemy import text from app.db import engine from app.webhooks_chatwoot import process_saved_chatwoot_raw_event def _as_dict(value: Any) -> dict[str, Any]: if isinstance(value, dict): return value if isinstance(value, str): try: parsed = json.loads(value) return parsed if isinstance(parsed, dict) else {} except Exception: return {} return {} def _preview(value: Any, limit: int = 180) -> str: text_value = " ".join(str(value or "").replace("\r", " ").replace("\n", " ").split()) return text_value if len(text_value) <= limit else text_value[: limit - 1].rstrip() + "…" def load_events(*, limit: int, include_errors: bool, source_event_id: str | None) -> list[dict[str, Any]]: where = [ "source_system = 'chatwoot'", "event_type = 'message_created'", "COALESCE(payload #>> '{message_type}', '') = 'incoming'", ] params: dict[str, Any] = {"limit": int(limit)} if source_event_id: where.append("source_event_id = :source_event_id") params["source_event_id"] = str(source_event_id) elif include_errors: where.append("processed = FALSE") where.append("ignored = FALSE") where.append("processing_error IS NOT NULL") else: where.append("processed = FALSE") where.append("ignored = FALSE") where.append("processing_error IS NULL") sql = text(f""" SELECT id::text, source_event_id, created_at, conversation_id, contact_id, processing_error, payload FROM raw_events WHERE {' AND '.join(where)} ORDER BY created_at ASC LIMIT :limit """) with engine.begin() as conn: return [dict(row) for row in conn.execute(sql, params).mappings().all()] async def main_async() -> int: parser = argparse.ArgumentParser() parser.add_argument("--limit", type=int, default=50, help="Máximo de eventos a reprocessar") parser.add_argument("--dry-run", action="store_true", help="Lista eventos sem processar") parser.add_argument("--include-errors", action="store_true", help="Reprocessa eventos com processing_error") parser.add_argument("--source-event-id", help="Reprocessa só uma mensagem Chatwoot específica") args = parser.parse_args() rows = load_events(limit=args.limit, include_errors=args.include_errors, source_event_id=args.source_event_id) print(f"Eventos Chatwoot incoming selecionados: {len(rows)}") for row in rows[:80]: payload = _as_dict(row.get("payload")) print( f"- {row['created_at']} raw={row['id']} msg={row.get('source_event_id')} " f"conv={payload.get('conversation', {}).get('id') or row.get('conversation_id')} " f"email={payload.get('sender', {}).get('email') or '-'} " f"err={row.get('processing_error') or '-'} " f"content={_preview(payload.get('content'))}" ) if args.dry_run or not rows: print("Dry-run: nenhuma alteração aplicada." if args.dry_run else "Nada para processar.") return 0 summary: dict[str, int] = {} for row in rows: payload = _as_dict(row.get("payload")) try: result = await process_saved_chatwoot_raw_event(row["id"], payload) status = str(result.get("status") or "processed") except Exception as exc: status = f"exception:{type(exc).__name__}" print(f"ERRO raw={row['id']} msg={row.get('source_event_id')}: {exc}") summary[status] = summary.get(status, 0) + 1 print("\nResumo:") for status, total in sorted(summary.items(), key=lambda item: (-item[1], item[0])): print(f"{status}: {total}") return 0 def main() -> int: return asyncio.run(main_async()) if __name__ == "__main__": raise SystemExit(main())