apps/whatsapp_control_api/app/services/meta_whatsapp.py
text
from __future__ import annotations
import logging
import httpx
from ..core.config import get_settings
logger = logging.getLogger(__name__)
class MetaWhatsAppSender:
@property
def settings(self):
return get_settings()
@property
def enabled(self) -> bool:
return bool(self.settings.meta_phone_number_id and self.settings.meta_access_token)
async def send_text(self, to_number: str, body: str) -> dict | None:
if not self.enabled:
logger.warning("Meta WhatsApp sender disabled because credentials are incomplete.")
return None
truncated_body = body[: self.settings.max_outbound_text_length]
url = (
f"https://graph.facebook.com/"
f"{self.settings.meta_graph_api_version}/"
f"{self.settings.meta_phone_number_id}/messages"
)
headers = {
"Authorization": f"Bearer {self.settings.meta_access_token}",
"Content-Type": "application/json",
}
payload = {
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": to_number,
"type": "text",
"text": {
"preview_url": False,
"body": truncated_body,
},
}
async with httpx.AsyncClient(timeout=15.0) as client:
try:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
except httpx.HTTPStatusError as exc:
status_code = exc.response.status_code
response_text = exc.response.text[:1000]
logger.error(
"Meta WhatsApp send failed: status=%s phone_number_id=%s response=%s",
status_code,
self.settings.meta_phone_number_id,
response_text,
)
return None
except httpx.HTTPError as exc:
logger.error(
"Meta WhatsApp send failed: phone_number_id=%s error=%s",
self.settings.meta_phone_number_id,
exc,
)
return None
return response.json()
async def mark_read(self, message_id: str, *, typing: bool = False) -> dict | None:
if not self.enabled:
logger.warning("Meta WhatsApp sender disabled because credentials are incomplete.")
return None
url = (
f"https://graph.facebook.com/"
f"{self.settings.meta_graph_api_version}/"
f"{self.settings.meta_phone_number_id}/messages"
)
headers = {
"Authorization": f"Bearer {self.settings.meta_access_token}",
"Content-Type": "application/json",
}
payload: dict = {
"messaging_product": "whatsapp",
"status": "read",
"message_id": message_id,
}
if typing:
payload["typing_indicator"] = {"type": "text"}
async with httpx.AsyncClient(timeout=15.0) as client:
try:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
except httpx.HTTPStatusError as exc:
logger.error(
"Meta WhatsApp mark-read failed: status=%s phone_number_id=%s response=%s",
exc.response.status_code,
self.settings.meta_phone_number_id,
exc.response.text[:1000],
)
return None
except httpx.HTTPError as exc:
logger.error(
"Meta WhatsApp mark-read failed: phone_number_id=%s error=%s",
self.settings.meta_phone_number_id,
exc,
)
return None
return response.json()