88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
|
|
import hashlib
|
||
|
|
import hmac
|
||
|
|
|
||
|
|
from fastapi import HTTPException, status
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.core.config import settings
|
||
|
|
from app.modules.business.models import Business
|
||
|
|
from app.modules.whatsapp.schemas import ConnectRequest, WebhookPayload
|
||
|
|
|
||
|
|
|
||
|
|
def verify_signature(payload_bytes: bytes, signature_header: str) -> None:
|
||
|
|
"""Valida X-Hub-Signature-256 enviado por Meta."""
|
||
|
|
if not signature_header or not signature_header.startswith("sha256="):
|
||
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Firma ausente")
|
||
|
|
|
||
|
|
expected = hmac.new(
|
||
|
|
settings.META_APP_SECRET.encode(),
|
||
|
|
payload_bytes,
|
||
|
|
hashlib.sha256,
|
||
|
|
).hexdigest()
|
||
|
|
|
||
|
|
if not hmac.compare_digest(expected, signature_header[len("sha256="):]):
|
||
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Firma inválida")
|
||
|
|
|
||
|
|
|
||
|
|
async def get_business_by_phone_number_id(
|
||
|
|
db: AsyncSession, phone_number_id: str
|
||
|
|
) -> Business | None:
|
||
|
|
result = await db.execute(
|
||
|
|
select(Business).where(Business.whatsapp_phone_number_id == phone_number_id)
|
||
|
|
)
|
||
|
|
return result.scalar_one_or_none()
|
||
|
|
|
||
|
|
|
||
|
|
async def connect_whatsapp(
|
||
|
|
db: AsyncSession, business_id: int, data: ConnectRequest
|
||
|
|
) -> Business:
|
||
|
|
result = await db.execute(select(Business).where(Business.id == business_id))
|
||
|
|
business = result.scalar_one_or_none()
|
||
|
|
if not business:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Negocio no encontrado")
|
||
|
|
|
||
|
|
business.whatsapp_access_token = data.access_token
|
||
|
|
business.whatsapp_phone_number_id = data.phone_number_id
|
||
|
|
business.meta_business_id = data.meta_business_id
|
||
|
|
await db.commit()
|
||
|
|
await db.refresh(business)
|
||
|
|
return business
|
||
|
|
|
||
|
|
|
||
|
|
async def disconnect_whatsapp(db: AsyncSession, business_id: int) -> None:
|
||
|
|
result = await db.execute(select(Business).where(Business.id == business_id))
|
||
|
|
business = result.scalar_one_or_none()
|
||
|
|
if not business:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Negocio no encontrado")
|
||
|
|
|
||
|
|
business.whatsapp_access_token = None
|
||
|
|
business.whatsapp_phone_number_id = None
|
||
|
|
business.meta_business_id = None
|
||
|
|
await db.commit()
|
||
|
|
|
||
|
|
|
||
|
|
async def dispatch_webhook(db: AsyncSession, payload: WebhookPayload) -> None:
|
||
|
|
"""Procesa cada mensaje entrante y lo envía al bot engine."""
|
||
|
|
from app.modules.bot_engine.service import process_message
|
||
|
|
|
||
|
|
for entry in payload.entry:
|
||
|
|
for change in entry.changes:
|
||
|
|
if change.field != "messages" or not change.value.messages:
|
||
|
|
continue
|
||
|
|
|
||
|
|
phone_number_id = change.value.metadata.get("phone_number_id")
|
||
|
|
business = await get_business_by_phone_number_id(db, phone_number_id)
|
||
|
|
if not business:
|
||
|
|
continue
|
||
|
|
|
||
|
|
for message in change.value.messages:
|
||
|
|
if message.type != "text" or not message.text:
|
||
|
|
continue
|
||
|
|
await process_message(
|
||
|
|
db=db,
|
||
|
|
phone=message.from_,
|
||
|
|
text=message.text.body,
|
||
|
|
business=business,
|
||
|
|
)
|