Files
WhatsAppCentralizado/services/api-gateway/app/routers/whatsapp.py
Claude AI dcb7fb5974 feat: add Layer 3 - API Gateway main app, WhatsApp routes, Frontend pages
API Gateway:
- main.py with FastAPI app, CORS, health endpoints
- WhatsApp routes: accounts CRUD, conversations, messages, internal events
- WhatsApp schemas for request/response validation

Frontend:
- Login page with register option for first admin
- MainLayout with sidebar navigation and user dropdown
- Dashboard with statistics cards (accounts, conversations)
- WhatsApp Accounts page with QR modal for connection
- Inbox page with conversation list and real-time chat

Full feature set for Fase 1 Foundation complete.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 10:01:06 +00:00

276 lines
8.9 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from uuid import UUID
import httpx
from app.core.database import get_db
from app.core.config import get_settings
from app.core.security import get_current_user
from app.models.user import User, UserRole
from app.models.whatsapp import (
WhatsAppAccount, Contact, Conversation, Message,
AccountStatus, MessageDirection, MessageType, MessageStatus, ConversationStatus
)
from app.schemas.whatsapp import (
WhatsAppAccountCreate, WhatsAppAccountResponse,
ConversationResponse, ConversationDetailResponse,
SendMessageRequest, MessageResponse, InternalEventRequest
)
router = APIRouter(prefix="/api/whatsapp", tags=["whatsapp"])
settings = get_settings()
def require_admin(current_user: User = Depends(get_current_user)):
if current_user.role != UserRole.ADMIN:
raise HTTPException(status_code=403, detail="Admin required")
return current_user
@router.post("/accounts", response_model=WhatsAppAccountResponse)
async def create_account(
request: WhatsAppAccountCreate,
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
account = WhatsAppAccount(name=request.name)
db.add(account)
db.commit()
db.refresh(account)
# Start session in WhatsApp Core
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{settings.WHATSAPP_CORE_URL}/api/sessions",
json={"accountId": str(account.id), "name": account.name},
timeout=30,
)
if response.status_code == 200:
data = response.json()
account.qr_code = data.get("qrCode")
account.status = AccountStatus(data.get("status", "connecting"))
db.commit()
except Exception:
pass
return account
@router.get("/accounts", response_model=List[WhatsAppAccountResponse])
def list_accounts(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
accounts = db.query(WhatsAppAccount).all()
return accounts
@router.get("/accounts/{account_id}", response_model=WhatsAppAccountResponse)
async def get_account(
account_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
account = db.query(WhatsAppAccount).filter(WhatsAppAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Account not found")
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{account_id}",
timeout=10,
)
if response.status_code == 200:
data = response.json()
account.qr_code = data.get("qrCode")
account.status = AccountStatus(data.get("status", "disconnected"))
account.phone_number = data.get("phoneNumber")
db.commit()
except Exception:
pass
return account
@router.delete("/accounts/{account_id}")
async def delete_account(
account_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
account = db.query(WhatsAppAccount).filter(WhatsAppAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Account not found")
async with httpx.AsyncClient() as client:
try:
await client.delete(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{account_id}",
timeout=10,
)
except Exception:
pass
db.delete(account)
db.commit()
return {"success": True}
@router.get("/conversations", response_model=List[ConversationResponse])
def list_conversations(
status: ConversationStatus = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = db.query(Conversation)
if status:
query = query.filter(Conversation.status == status)
conversations = query.order_by(Conversation.last_message_at.desc()).limit(50).all()
return conversations
@router.get("/conversations/{conversation_id}", response_model=ConversationDetailResponse)
def get_conversation(
conversation_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
conversation = db.query(Conversation).filter(Conversation.id == conversation_id).first()
if not conversation:
raise HTTPException(status_code=404, detail="Conversation not found")
return conversation
@router.post("/conversations/{conversation_id}/messages", response_model=MessageResponse)
async def send_message(
conversation_id: UUID,
request: SendMessageRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
conversation = db.query(Conversation).filter(Conversation.id == conversation_id).first()
if not conversation:
raise HTTPException(status_code=404, detail="Conversation not found")
message = Message(
conversation_id=conversation.id,
direction=MessageDirection.OUTBOUND,
type=request.type,
content=request.content,
media_url=request.media_url,
sent_by=current_user.id,
status=MessageStatus.PENDING,
)
db.add(message)
db.commit()
db.refresh(message)
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{conversation.whatsapp_account_id}/messages",
json={
"to": conversation.contact.phone_number,
"type": request.type.value,
"content": {"text": request.content} if request.type == MessageType.TEXT else {
"url": request.media_url,
"caption": request.content,
},
},
timeout=30,
)
if response.status_code == 200:
data = response.json()
message.whatsapp_message_id = data.get("messageId")
message.status = MessageStatus.SENT
else:
message.status = MessageStatus.FAILED
except Exception:
message.status = MessageStatus.FAILED
db.commit()
db.refresh(message)
return message
# Internal endpoint for WhatsApp Core events
@router.post("/internal/whatsapp/event")
async def handle_whatsapp_event(
event: InternalEventRequest,
db: Session = Depends(get_db),
):
account = db.query(WhatsAppAccount).filter(
WhatsAppAccount.id == event.accountId
).first()
if not account:
return {"status": "ignored", "reason": "account not found"}
if event.type == "qr":
account.qr_code = event.data.get("qrCode")
account.status = AccountStatus.CONNECTING
elif event.type == "connected":
account.status = AccountStatus.CONNECTED
account.phone_number = event.data.get("phoneNumber")
account.qr_code = None
elif event.type == "disconnected":
account.status = AccountStatus.DISCONNECTED
account.qr_code = None
elif event.type == "message":
msg_data = event.data
phone = msg_data.get("from", "").split("@")[0]
contact = db.query(Contact).filter(Contact.phone_number == phone).first()
if not contact:
contact = Contact(
phone_number=phone,
name=msg_data.get("pushName"),
)
db.add(contact)
db.commit()
db.refresh(contact)
conversation = db.query(Conversation).filter(
Conversation.whatsapp_account_id == account.id,
Conversation.contact_id == contact.id,
Conversation.status != ConversationStatus.RESOLVED,
).first()
if not conversation:
conversation = Conversation(
whatsapp_account_id=account.id,
contact_id=contact.id,
status=ConversationStatus.BOT,
)
db.add(conversation)
db.commit()
db.refresh(conversation)
wa_message = msg_data.get("message", {})
content = (
wa_message.get("conversation") or
wa_message.get("extendedTextMessage", {}).get("text") or
"[Media]"
)
message = Message(
conversation_id=conversation.id,
whatsapp_message_id=msg_data.get("id"),
direction=MessageDirection.INBOUND,
type=MessageType.TEXT,
content=content,
status=MessageStatus.DELIVERED,
)
db.add(message)
from datetime import datetime
conversation.last_message_at = datetime.utcnow()
db.commit()
return {"status": "ok"}