Instrument webhooks, fix compose layout, and ship social connect.
Add Grafana-friendly webhook request logging, Quill overflow fix, New Contact flow, in-place postcard campaign compose, and functional social account connect + composer.
This commit is contained in:
+102
-3
@@ -286,6 +286,76 @@ def _webhook_authorized(request, *, secret: str = "", secrets: list[str] | None
|
||||
return False
|
||||
|
||||
|
||||
def _log_webhook_request(request, *, channel: str) -> None:
|
||||
"""Full request dump for Grafana / log aggregation."""
|
||||
try:
|
||||
headers = {str(k): str(v) for k, v in request.headers.items()}
|
||||
except Exception: # noqa: BLE001
|
||||
headers = {"_error": "unable to serialize headers"}
|
||||
try:
|
||||
body_text = (request.body or b"").decode("utf-8", errors="replace")
|
||||
except Exception: # noqa: BLE001
|
||||
body_text = repr(request.body)
|
||||
if len(body_text) > 12000:
|
||||
body_text = body_text[:12000] + "…[truncated]"
|
||||
logger.info(
|
||||
"webhook_received channel=%s path=%s method=%s query=%s",
|
||||
channel,
|
||||
request.path,
|
||||
request.method,
|
||||
request.META.get("QUERY_STRING", ""),
|
||||
)
|
||||
logger.info("webhook_headers channel=%s headers=%s", channel, headers)
|
||||
logger.info("webhook_body channel=%s body=%s", channel, body_text)
|
||||
|
||||
|
||||
def _log_webhook_auth_failed(request, *, channel: str) -> None:
|
||||
logger.warning(
|
||||
"webhook_auth_failed channel=%s path=%s "
|
||||
"missing_or_invalid_authorization_or_token",
|
||||
channel,
|
||||
request.path,
|
||||
)
|
||||
|
||||
|
||||
def _log_webhook_result(
|
||||
*,
|
||||
channel: str,
|
||||
event=None,
|
||||
error: str = "",
|
||||
extra: str = "",
|
||||
) -> None:
|
||||
if error:
|
||||
logger.error(
|
||||
"webhook_error channel=%s error=%s %s",
|
||||
channel,
|
||||
error,
|
||||
extra,
|
||||
)
|
||||
return
|
||||
if not event:
|
||||
logger.warning(
|
||||
"webhook_unmatched channel=%s no_provider_event_created %s",
|
||||
channel,
|
||||
extra,
|
||||
)
|
||||
return
|
||||
message = getattr(event, "message", None)
|
||||
campaign = getattr(message, "campaign", None) if message else None
|
||||
logger.info(
|
||||
"webhook_processed channel=%s event_type=%s event_id=%s "
|
||||
"matched=%s message_id=%s campaign_id=%s campaign_name=%s %s",
|
||||
channel,
|
||||
getattr(event, "event_type", ""),
|
||||
getattr(event, "pk", None),
|
||||
bool(message),
|
||||
getattr(message, "pk", None),
|
||||
getattr(campaign, "pk", None),
|
||||
getattr(campaign, "name", "") or "",
|
||||
extra,
|
||||
)
|
||||
|
||||
|
||||
def _pcm_webhook_secrets() -> list[str]:
|
||||
"""All PCM subscription signature secrets from env."""
|
||||
raw_list = (getattr(settings, "PCM_WEBHOOK_SECRETS", None) or "").strip()
|
||||
@@ -675,12 +745,21 @@ def postcard_webhook(request):
|
||||
URL: https://<host>/portal/messaging/webhooks/postcard/
|
||||
Copy each subscription's signature secret into PCM_WEBHOOK_SECRETS
|
||||
"""
|
||||
channel = "postcard"
|
||||
_log_webhook_request(request, channel=channel)
|
||||
if not _webhook_authorized(request, secrets=_pcm_webhook_secrets()):
|
||||
_log_webhook_auth_failed(request, channel=channel)
|
||||
return HttpResponseForbidden("invalid webhook token")
|
||||
payload = parse_webhook_payload(request)
|
||||
if not payload:
|
||||
payload = request.POST.dict() or {}
|
||||
event = process_pcm_postcard_webhook(payload)
|
||||
try:
|
||||
event = process_pcm_postcard_webhook(payload)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.exception("PCM postcard webhook processing failed")
|
||||
_log_webhook_result(channel=channel, error=str(exc))
|
||||
return JsonResponse({"ok": False, "error": "processing_failed"}, status=200)
|
||||
_log_webhook_result(channel=channel, event=event)
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
@@ -705,9 +784,12 @@ def sms_webhook(request):
|
||||
|
||||
Inbound gateway POSTs without ``event`` (text=STOP, from=…) still opt out.
|
||||
"""
|
||||
channel = "sms"
|
||||
_log_webhook_request(request, channel=channel)
|
||||
if not _webhook_authorized(
|
||||
request, secret=settings.SMTP2GO_WEBHOOK_SECRET or ""
|
||||
):
|
||||
_log_webhook_auth_failed(request, channel=channel)
|
||||
return HttpResponseForbidden("invalid webhook token")
|
||||
|
||||
payload = parse_webhook_payload(request)
|
||||
@@ -724,9 +806,21 @@ def sms_webhook(request):
|
||||
or ""
|
||||
)
|
||||
stopped = bool(phone) and record_sms_stop(str(phone))
|
||||
logger.info(
|
||||
"webhook_processed channel=sms event_type=inbound_stop "
|
||||
"opt_out=%s phone=%s",
|
||||
stopped,
|
||||
phone,
|
||||
)
|
||||
return JsonResponse({"ok": True, "opt_out": stopped})
|
||||
|
||||
event = process_smtp2go_sms_webhook(payload)
|
||||
try:
|
||||
event = process_smtp2go_sms_webhook(payload)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.exception("SMTP2GO SMS webhook processing failed")
|
||||
_log_webhook_result(channel=channel, error=str(exc))
|
||||
return JsonResponse({"ok": False, "error": "processing_failed"}, status=200)
|
||||
_log_webhook_result(channel=channel, event=event)
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
@@ -749,16 +843,21 @@ def email_webhook(request):
|
||||
Email events: all delivery/engagement boxes
|
||||
Email headers: X-Monica-Message-Id
|
||||
"""
|
||||
channel = "email"
|
||||
_log_webhook_request(request, channel=channel)
|
||||
if not _webhook_authorized(
|
||||
request, secret=settings.SMTP2GO_WEBHOOK_SECRET or ""
|
||||
):
|
||||
_log_webhook_auth_failed(request, channel=channel)
|
||||
return HttpResponseForbidden("invalid webhook token")
|
||||
payload = parse_webhook_payload(request)
|
||||
try:
|
||||
event = process_smtp2go_email_webhook(payload)
|
||||
except Exception: # noqa: BLE001 — never 500 SMTP2GO (they retry for 48h)
|
||||
except Exception as exc: # noqa: BLE001 — never 500 SMTP2GO (they retry for 48h)
|
||||
logger.exception("SMTP2GO email webhook processing failed")
|
||||
_log_webhook_result(channel=channel, error=str(exc))
|
||||
return JsonResponse({"ok": False, "error": "processing_failed"}, status=200)
|
||||
_log_webhook_result(channel=channel, event=event)
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
|
||||
Reference in New Issue
Block a user