privacy policy and business integration
This commit is contained in:
parent
141ca9fda3
commit
cd3751219d
16 changed files with 1592 additions and 30 deletions
180
backend/app/whatsapp_messages.py
Normal file
180
backend/app/whatsapp_messages.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import re
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .config import settings
|
||||
from .models import WhatsAppMessage
|
||||
|
||||
PHONE_DIGITS_RE = re.compile(r"^\d{10,15}$")
|
||||
|
||||
|
||||
def normalize_wa_number(value: str) -> str:
|
||||
digits = re.sub(r"\D", "", value.strip())
|
||||
if len(digits) == 10:
|
||||
digits = f"91{digits}"
|
||||
if not PHONE_DIGITS_RE.match(digits):
|
||||
raise ValueError("Invalid WhatsApp number")
|
||||
return digits
|
||||
|
||||
|
||||
def business_phone() -> str:
|
||||
return normalize_wa_number(settings.whatsapp_business_phone)
|
||||
|
||||
|
||||
def _media_from_payload(message_type: str, payload: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if not payload or message_type not in {"image", "audio", "video", "document", "sticker"}:
|
||||
return None
|
||||
media = payload.get(message_type) or {}
|
||||
if not isinstance(media, dict):
|
||||
return None
|
||||
return {
|
||||
"id": media.get("id"),
|
||||
"mime_type": media.get("mime_type"),
|
||||
"caption": media.get("caption"),
|
||||
"filename": media.get("filename"),
|
||||
}
|
||||
|
||||
|
||||
def serialize_message(message: WhatsAppMessage) -> dict:
|
||||
payload = message.payload if isinstance(message.payload, dict) else None
|
||||
media = _media_from_payload(message.message_type, payload)
|
||||
return {
|
||||
"id": str(message.id),
|
||||
"direction": message.direction,
|
||||
"wa_message_id": message.wa_message_id,
|
||||
"from": message.from_number,
|
||||
"to": message.to_number,
|
||||
"body": message.body,
|
||||
"message_type": message.message_type,
|
||||
"status": message.status,
|
||||
"media": media,
|
||||
"created_at": message.created_at,
|
||||
}
|
||||
|
||||
|
||||
def _extract_text_body(message: dict[str, Any]) -> str | None:
|
||||
msg_type = message.get("type")
|
||||
if msg_type == "text":
|
||||
return (message.get("text") or {}).get("body")
|
||||
if msg_type == "button":
|
||||
return (message.get("button") or {}).get("text")
|
||||
if msg_type == "interactive":
|
||||
interactive = message.get("interactive") or {}
|
||||
if interactive.get("type") == "button_reply":
|
||||
return (interactive.get("button_reply") or {}).get("title")
|
||||
if interactive.get("type") == "list_reply":
|
||||
return (interactive.get("list_reply") or {}).get("title")
|
||||
if msg_type in {"image", "video", "document"}:
|
||||
return (message.get(msg_type) or {}).get("caption")
|
||||
if msg_type == "reaction":
|
||||
reaction = message.get("reaction") or {}
|
||||
emoji = reaction.get("emoji")
|
||||
return f"Reacted {emoji}" if emoji else "Reaction"
|
||||
if msg_type == "location":
|
||||
location = message.get("location") or {}
|
||||
name = location.get("name") or location.get("address")
|
||||
if name:
|
||||
return f"Location: {name}"
|
||||
lat = location.get("latitude")
|
||||
lng = location.get("longitude")
|
||||
if lat is not None and lng is not None:
|
||||
return f"Location: {lat}, {lng}"
|
||||
if msg_type == "contacts":
|
||||
contacts = message.get("contacts") or []
|
||||
if contacts:
|
||||
name = ((contacts[0].get("name") or {}).get("formatted_name")) or "Contact"
|
||||
return f"Contact: {name}"
|
||||
return None
|
||||
|
||||
|
||||
def record_inbound_messages(db: Session, payload: dict[str, Any]) -> int:
|
||||
if payload.get("object") != "whatsapp_business_account":
|
||||
return 0
|
||||
|
||||
business = business_phone()
|
||||
created = 0
|
||||
for entry in payload.get("entry", []):
|
||||
for change in entry.get("changes", []):
|
||||
if change.get("field") != "messages":
|
||||
continue
|
||||
value = change.get("value", {})
|
||||
for message in value.get("messages", []):
|
||||
from_number = message.get("from") or ""
|
||||
body = _extract_text_body(message)
|
||||
db.add(
|
||||
WhatsAppMessage(
|
||||
direction="inbound",
|
||||
wa_message_id=message.get("id"),
|
||||
from_number=from_number,
|
||||
to_number=business,
|
||||
body=body,
|
||||
message_type=message.get("type") or "text",
|
||||
status="received",
|
||||
payload=message,
|
||||
)
|
||||
)
|
||||
created += 1
|
||||
for status in value.get("statuses", []):
|
||||
wa_id = status.get("id")
|
||||
if not wa_id:
|
||||
continue
|
||||
existing = db.scalar(
|
||||
select(WhatsAppMessage).where(WhatsAppMessage.wa_message_id == wa_id)
|
||||
)
|
||||
if existing:
|
||||
existing.status = status.get("status") or existing.status
|
||||
else:
|
||||
db.add(
|
||||
WhatsAppMessage(
|
||||
direction="outbound",
|
||||
wa_message_id=wa_id,
|
||||
from_number=business,
|
||||
to_number=status.get("recipient_id") or "",
|
||||
body=None,
|
||||
message_type="status",
|
||||
status=status.get("status") or "unknown",
|
||||
payload=status,
|
||||
)
|
||||
)
|
||||
created += 1
|
||||
if created:
|
||||
db.commit()
|
||||
return created
|
||||
|
||||
|
||||
def record_outbound_message(
|
||||
db: Session,
|
||||
*,
|
||||
to_number: str,
|
||||
body: str | None,
|
||||
message_type: str = "text",
|
||||
wa_message_id: str | None,
|
||||
status: str = "sent",
|
||||
payload: dict | None = None,
|
||||
) -> WhatsAppMessage:
|
||||
message = WhatsAppMessage(
|
||||
direction="outbound",
|
||||
wa_message_id=wa_message_id,
|
||||
from_number=business_phone(),
|
||||
to_number=to_number,
|
||||
body=body,
|
||||
message_type=message_type,
|
||||
status=status,
|
||||
payload=payload,
|
||||
)
|
||||
db.add(message)
|
||||
db.commit()
|
||||
db.refresh(message)
|
||||
return message
|
||||
|
||||
|
||||
def extract_wa_message_id(result: dict[str, Any] | None) -> str | None:
|
||||
if not isinstance(result, dict):
|
||||
return None
|
||||
messages = result.get("messages")
|
||||
if isinstance(messages, list) and messages:
|
||||
message_id = messages[0].get("id")
|
||||
return message_id if isinstance(message_id, str) else None
|
||||
return None
|
||||
Loading…
Add table
Add a link
Reference in a new issue