whatsapp broadcast

This commit is contained in:
debu-2 2026-10-13 03:14:52 +05:30
parent 63011f7d96
commit 70ccb720aa
9 changed files with 570 additions and 4 deletions

View file

@ -1027,6 +1027,60 @@ async def list_templates(db: Session, user: UserAccount, account_id: Any) -> dic
return {"available": True, "templates": templates}
_TEMPLATE_NAME = re.compile(r"^[a-z0-9_]{1,512}$")
async def create_marketing_template(
db: Session,
user: UserAccount,
account_id: Any,
name: str,
language: str,
body: str,
) -> dict[str, Any]:
"""Submit a marketing template to Meta for the connected WhatsApp account."""
account = _owned_account(db, user, account_id)
connection = account.connection
token = connection.access_token if connection else None
if connection is None or not token or connection.status == "disconnected":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Save the access token again before creating a template.")
template_name = name.strip().lower()
template_language = language.strip() or "en"
text = body.strip()
if not _TEMPLATE_NAME.match(template_name):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Template name should use lowercase letters, numbers, and underscores.",
)
if not re.fullmatch(r"[a-z]{2}(_[A-Z]{2})?", template_language):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Language should look like en or en_US.")
if not text or len(text) > 1024:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Template text should be 1 to 1024 characters.")
if "{{" in text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="This test template cannot include placeholders such as {{1}}.",
)
data = await _graph(
"POST",
f"{account.waba_id}/message_templates",
token=token,
json_body={
"name": template_name,
"language": template_language,
"category": "MARKETING",
"components": [{"type": "BODY", "text": text}],
},
)
return {
"id": data.get("id"),
"name": template_name,
"language": template_language,
"category": str(data.get("category") or "MARKETING"),
"status": str(data.get("status") or "PENDING"),
}
def account_logs(db: Session, user: UserAccount, account_id: Any) -> dict[str, Any]:
account = _owned_account(db, user, account_id)
audits = db.scalars(
@ -1127,6 +1181,31 @@ def _messaging_phone(account: WhatsAppBusinessAccount) -> tuple[WhatsAppPhoneAss
return phone, token
async def enable_customer_delivery(db: Session, user: UserAccount, account_id: Any, app_secret: str) -> dict[str, Any]:
"""Point this account's replies and delivery updates at the workspace."""
account = _owned_account(db, user, account_id)
connection = account.connection
token = connection.access_token if connection else None
if connection is None or not token or connection.status == "disconnected":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Save the access token again before replies can arrive here.")
secret = "".join(str(app_secret or "").split())
if len(secret) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Paste the App secret from the Meta app that created this token.",
)
connection.meta_app_secret = secret
phone_ids = [phone.phone_number_id for phone in account.phones]
receiving = await _attach_customer_webhook(token, account.waba_id, phone_ids)
account.webhooks_status = "active" if receiving else "unavailable"
account.last_error = None if receiving else "Meta did not accept the delivery address for replies."
db.commit()
db.refresh(account)
if not receiving:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=account.last_error)
return serialize_account(account)
async def send_account_message(db: Session, user: UserAccount, account_id: Any, payload: dict[str, Any]) -> dict[str, Any]:
account = _owned_account(db, user, account_id)
phone, token = _messaging_phone(account)
@ -1201,6 +1280,93 @@ async def send_account_message(db: Session, user: UserAccount, account_id: Any,
return serialize_message(message)
async def send_account_broadcast(
db: Session,
user: UserAccount,
account_id: Any,
template_name: str,
template_language: str,
recipients: list[str],
body_parameters: list[str] | None = None,
product_policy: str = "STRICT",
) -> dict[str, Any]:
"""Send a marketing template through Meta's marketing endpoint."""
account = _owned_account(db, user, account_id)
phone, token = _messaging_phone(account)
name = template_name.strip()
language = template_language.strip() or "en"
policy = product_policy if product_policy in {"STRICT", "CLOUD_API_FALLBACK"} else "STRICT"
numbers = []
for raw in recipients:
piece = str(raw or "").strip()
if piece:
numbers.append(piece)
if not name:
raise HTTPException(status_code=422, detail="Choose a marketing template.")
if not numbers:
raise HTTPException(status_code=422, detail="Add at least one recipient.")
if len(numbers) > 20:
raise HTTPException(status_code=422, detail="A test broadcast can include up to 20 numbers.")
parameters = [str(item).strip() for item in (body_parameters or []) if str(item).strip()]
components = None
if parameters:
components = [
{
"type": "body",
"parameters": [{"type": "text", "text": item} for item in parameters],
}
]
client = WhatsAppGraphClient(
access_token=token,
phone_number_id=phone.phone_number_id,
business_account_id=account.waba_id,
)
try:
from_number = normalize_wa_number(phone.display_phone or "")
except ValueError:
from_number = phone.display_phone or phone.phone_number_id
results: list[dict[str, Any]] = []
for raw in numbers:
try:
recipient = normalize_wa_number(raw)
except ValueError:
results.append({"to": raw, "status": "failed", "error": "Enter a valid WhatsApp number."})
continue
try:
result = await client.send_marketing_template(
recipient,
name,
language_code=language,
components=components,
product_policy=policy,
)
except WhatsAppGraphError as exc:
results.append({"to": recipient, "status": "failed", "error": exc.detail})
continue
message = record_outbound_message(
db,
to_number=recipient,
body=f"Broadcast: {name}",
message_type="template",
wa_message_id=extract_wa_message_id(result),
status="sent",
payload=result if isinstance(result, dict) else None,
from_number=from_number,
user_id=user.id,
phone_number_id=phone.phone_number_id,
)
results.append({"to": recipient, "status": "sent", "message_id": message.wa_message_id})
sent = sum(1 for item in results if item["status"] == "sent")
return {
"template_name": name,
"template_language": language,
"product_policy": policy,
"sent": sent,
"failed": len(results) - sent,
"results": results,
}
async def download_account_media(db: Session, user: UserAccount, account_id: Any, media_id: str) -> tuple[bytes, str]:
account = _owned_account(db, user, account_id)
token = account.connection.access_token if account.connection else None