Tirando env do commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,168 @@
|
||||
# app/api/webhook.py
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, status
|
||||
from fastapi.responses import PlainTextResponse, JSONResponse, Response
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from config import VERIFY_TOKEN
|
||||
from models.webhook_model import WebhookEvent
|
||||
from services.webhook_service import (
|
||||
handle_message_type, mark_message_as_read,
|
||||
encrypt_flow_response_data, decrypt_flow_request_data, send_whatsapp_flow # <--- ADICIONAR decrypt_flow_request_data
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class SendFlowRequest(BaseModel):
|
||||
to_number: str # O número para onde enviar o Flow (ex: '5582912345678')
|
||||
|
||||
|
||||
# ... (Endpoint GET /webhook) ...
|
||||
|
||||
# --- Endpoint para Receber Requisições POST (Incluindo TODOS os Payloads Criptografados e Webhook Padrão) ---
|
||||
@router.post("/webhook")
|
||||
async def process_whatsapp_webhook(request: Request):
|
||||
try:
|
||||
body = await request.json()
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"⚠️ Requisição POST recebida com corpo não-JSON ou JSON inválido: {e}")
|
||||
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "corpo inválido, ignorado"})
|
||||
except Exception as e:
|
||||
print(f"⚠️ Erro inesperado ao ler o corpo da requisição POST: {e}")
|
||||
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "erro de leitura, ignorado"})
|
||||
|
||||
# --- Lógica para Requisições de Dados de Flow Criptografadas (incluindo Health Check criptografado) ---
|
||||
if isinstance(body, dict) and "encrypted_flow_data" in body and "encrypted_aes_key" in body and "initial_vector" in body:
|
||||
print("🔒 Recebido payload de Flow criptografado.")
|
||||
try:
|
||||
# Descriptografa a requisição para obter os dados, a chave AES e o IV originais
|
||||
decrypted_result = decrypt_flow_request_data(
|
||||
body["encrypted_flow_data"],
|
||||
body["encrypted_aes_key"],
|
||||
body["initial_vector"]
|
||||
)
|
||||
decrypted_data = decrypted_result["decrypted_payload"]
|
||||
aes_key_from_request = decrypted_result["aes_key"]
|
||||
iv_from_request = decrypted_result["initial_vector"]
|
||||
|
||||
print(f"🔓 Dados do Flow descriptografados: {json.dumps(decrypted_data, indent=2)}")
|
||||
|
||||
flow_response_data = {
|
||||
"data": {
|
||||
"status": "active"
|
||||
}
|
||||
}
|
||||
|
||||
# Processa o tipo de requisição descriptografada
|
||||
if decrypted_data.get("action") == "ping":
|
||||
print("💚 Descriptografado: Ping de dados do Flow. Preparando resposta 'active'.")
|
||||
else:
|
||||
print(f"❓ Descriptografado: Outros dados de Flow. Conteúdo: {decrypted_data}")
|
||||
flow_response_data = {"status": "success", "message": "Dados de Flow processados."}
|
||||
|
||||
# Criptografa a resposta usando a chave AES e o IV DA REQUISIÇÃO ORIGINAL
|
||||
# A função encrypt_flow_response_data AGORA RETORNA A STRING BASE64 DIRETA
|
||||
encrypted_final_response_string = encrypt_flow_response_data(
|
||||
flow_response_data,
|
||||
aes_key_from_request,
|
||||
iv_from_request
|
||||
)
|
||||
|
||||
print(f"DEBUG: Enviando resposta final criptografada (Base64): {encrypted_final_response_string[:50]}...")
|
||||
return Response(content=encrypted_final_response_string, media_type="text/plain")
|
||||
|
||||
except ValueError as ve:
|
||||
print(f"❌ ERRO de chave ou dados para descriptografia/criptografia de Flow: {ve}")
|
||||
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"status": f"Erro de chave ou criptografia: {ve}"})
|
||||
except Exception as e:
|
||||
print(f"❌ ERRO ao processar dados de Flow criptografados: {e}")
|
||||
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"status": f"Erro interno ao processar Flow: {e}"})
|
||||
# --- Lógica para Eventos de Webhook Padrão do WhatsApp (mensagens, status) ---
|
||||
# Este bloco só será acionado se não for um payload de Flow criptografado
|
||||
# --- Bloco para Eventos Padrão do WhatsApp (mensagens, status, etc.) ---
|
||||
# Este é o bloco que interessa para o teste de sessão
|
||||
elif isinstance(body, dict) and "object" in body and "entry" in body:
|
||||
try:
|
||||
event = WebhookEvent(**body)
|
||||
if event.object != 'whatsapp_business_account':
|
||||
print("❌ Evento recebido, mas não é do tipo 'whatsapp_business_account'. Ignorando.")
|
||||
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={"status": "ignorado"})
|
||||
|
||||
for entry in event.entry:
|
||||
for change in entry.changes:
|
||||
if change.field == 'messages':
|
||||
if change.value and change.value.messages:
|
||||
for message in change.value.messages:
|
||||
print(f"\n💬 Mensagem recebida de {message.from_} (Tipo: {message.type}, ID: {message.id})")
|
||||
await mark_message_as_read(message.id)
|
||||
# CHAME handle_message_type PASSANDO O OBJETO 'message' COMPLETO
|
||||
await handle_message_type(message)
|
||||
else:
|
||||
print("⚠️ Evento de 'messages' recebido, mas sem mensagens válidas no payload.")
|
||||
elif change.field == 'statuses':
|
||||
if change.value and change.value.statuses:
|
||||
for status_event in change.value.statuses:
|
||||
print(f"ℹ️ Status da mensagem ID {status_event.id}: {status_event.status} (para {status_event.recipient_id})")
|
||||
# Futuramente: handle_status_event(status_event)
|
||||
else:
|
||||
print("⚠️ Evento de 'statuses' recebido, mas sem status válidos no payload.")
|
||||
# REMOVA OU COMENTE TEMPORARIAMENTE o bloco 'elif change.field == 'flows':' se estiver lá
|
||||
# para focar apenas em mensagens/sessão.
|
||||
# elif change.field == 'flows':
|
||||
# ...
|
||||
else:
|
||||
print(f"⚠️ Evento de campo desconhecido recebido: {change.field}. Payload completo: {change.model_dump_json(indent=2) if hasattr(change, 'model_dump_json') else json.dumps(change.dict(), indent=2)}")
|
||||
|
||||
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "evento processado com sucesso"})
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Payload recebido não corresponde ao modelo WebhookEvent ou erro interno: {type(e).__name__}: {e}. Payload: {json.dumps(body)}")
|
||||
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "payload desconhecido, ignorado"})
|
||||
|
||||
# --- Fallback para payloads POST não reconhecidos ---
|
||||
else:
|
||||
print(f"❓ Payload POST recebido que não é Health Check, Flow criptografado, nem WebhookEvent padrão. Ignorando. Payload: {json.dumps(body)}")
|
||||
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "payload não reconhecido, ignorado"})
|
||||
|
||||
|
||||
@router.post("/send_cadastro_flow")
|
||||
async def trigger_cadastro_flow(request_data: SendFlowRequest):
|
||||
"""
|
||||
Dispara o Flow de cadastro do WhatsApp para um número específico.
|
||||
"""
|
||||
target_number = request_data.to_number # <-- ESTA LINHA DEVE ESTAR AQUI E ACESSÍVEL
|
||||
|
||||
# --- MUITO IMPORTANTE: Substitua pelo FLOW_ID REAL que você publicou ---
|
||||
FLOW_ID_DO_SEU_CADASTRO_PUBLICADO = 1094799999286205
|
||||
|
||||
if FLOW_ID_DO_SEU_CADASTRO_PUBLICADO == "COLOQUE_AQUI_O_FLOW_ID_DO_SEU_CADASTRO_PUBLICADO":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Por favor, substitua 'COLOQUE_AQUI_O_FLOW_ID_DO_SEU_CADASTRO_PUBLICADO' pelo Flow ID real no código."
|
||||
)
|
||||
|
||||
print(f"DEBUG: Disparando Flow de Cadastro (ID: {FLOW_ID_DO_SEU_CADASTRO_PUBLICADO}) para o número: {target_number}")
|
||||
|
||||
try:
|
||||
response = await send_whatsapp_flow(target_number, FLOW_ID_DO_SEU_CADASTRO_PUBLICADO, "Abrir Formulário de Cadastro")
|
||||
|
||||
if response and response.get("status") == "success":
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_200_OK,
|
||||
content={"message": f"Flow de cadastro enviado com sucesso para {target_number}", "whatsapp_api_response": response["data"]}
|
||||
)
|
||||
else:
|
||||
print(f"DEBUG: send_whatsapp_flow retornou erro: {response.get('message', 'Erro desconhecido')}")
|
||||
raise HTTPException(
|
||||
status_code=response.get("code", status.HTTP_500_INTERNAL_SERVER_ERROR),
|
||||
detail=f"Falha ao enviar Flow: {response.get('message', 'Erro desconhecido')}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"❌ ERRO INESPERADO na trigger_cadastro_flow: {type(e).__name__}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erro interno ao disparar Flow: {e}"
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Carrega as variáveis de ambiente do arquivo .env (na raiz do projeto)
|
||||
load_dotenv()
|
||||
|
||||
# --- Variáveis de Configuração ---
|
||||
VERIFY_TOKEN = os.getenv("WEBHOOK_VERIFICATION_TOKEN")
|
||||
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN")
|
||||
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
|
||||
FLOW_PRIVATE_KEY_PASSWORD = os.getenv("FLOW_PRIVATE_KEY_PASSWORD")
|
||||
|
||||
# Verificação para garantir que as variáveis críticas estão presentes
|
||||
if not VERIFY_TOKEN:
|
||||
raise ValueError("WEBHOOK_VERIFICATION_TOKEN não está definido no .env")
|
||||
if not WHATSAPP_ACCESS_TOKEN:
|
||||
raise ValueError("WHATSAPP_ACCESS_TOKEN não está definido no .env")
|
||||
if not WHATSAPP_PHONE_NUMBER_ID:
|
||||
raise ValueError("WHATSAPP_PHONE_NUMBER_ID não está definido no .env")
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# app/main.py
|
||||
|
||||
from fastapi import FastAPI
|
||||
from api import webhook
|
||||
import uvicorn
|
||||
from services.webhook_service import scheduler # <-- Importar o scheduler
|
||||
|
||||
app = FastAPI(
|
||||
title="Consultme WhatsApp API",
|
||||
description="API para integração com o WhatsApp Cloud API para a Consultme.",
|
||||
version="0.0.1",
|
||||
)
|
||||
|
||||
# Inclui as rotas definidas em app/api/webhook.py
|
||||
app.include_router(webhook.router)
|
||||
|
||||
# Rota de teste simples para verificar se a API está no ar (opcional)
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "API do WhatsApp Consultme está rodando!"}
|
||||
|
||||
# --- Eventos de Startup e Shutdown do FastAPI para o Scheduler ---
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
if not scheduler.running:
|
||||
scheduler.start()
|
||||
print("INFO: APScheduler iniciado na inicialização da aplicação.")
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
if scheduler.running:
|
||||
scheduler.shutdown()
|
||||
print("INFO: APScheduler encerrado no desligamento da aplicação.")
|
||||
# --- FIM DOS EVENTOS DO SCHEDULER ---
|
||||
|
||||
# Exemplo de uso de Uvicorn para rodar o FastAPI
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=5000)
|
||||
Binary file not shown.
@@ -0,0 +1,75 @@
|
||||
from typing import List, Optional, Union
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class TextMessage(BaseModel):
|
||||
body: str
|
||||
|
||||
class ButtonMessage(BaseModel):
|
||||
# O WhatsApp envia 'id' e 'title' para as respostas de botões,
|
||||
# mas o Pydantic pode esperar 'payload' e 'text' se for para envio.
|
||||
# Para *recebimento*, precisamos mapear o que vem.
|
||||
# O payload que você recebeu mostra 'id' e 'title'.
|
||||
|
||||
# Para a *resposta* de um botão:
|
||||
id: str # O ID do botão clicado (que você definiu no seu código)
|
||||
title: str # O texto do botão clicado
|
||||
|
||||
# No seu handle_button_response, você espera 'payload'.
|
||||
# Isso indica que ButtonMessage foi usado para ENVIO antes.
|
||||
# Para *recebimento*, a Meta envia 'id' e 'title'.
|
||||
# A melhor prática é ter dois modelos, um para envio e um para recebimento,
|
||||
# ou usar aliases condicionais.
|
||||
|
||||
# Pela sua mensagem de erro, o Pydantic (na validação do WebhookEvent)
|
||||
# esperava 'payload' e 'text' para o `button_reply`.
|
||||
# Vamos renomear e usar aliases para compatibilidade com o que a Meta envia E o que o seu handler espera.
|
||||
# O `id` do botão clicado é o `payload` que você espera no seu handler.
|
||||
# O `title` do botão clicado é o `text` que você espera.
|
||||
|
||||
# Modelagem para RECEBIMENTO de resposta de botão:
|
||||
payload: str = Field(alias="id") # Mapeia 'id' do JSON recebido para 'payload' no Python
|
||||
text: str = Field(alias="title") # Mapeia 'title' do JSON recebido para 'text' no Python
|
||||
|
||||
class ListReply(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
|
||||
class InteractiveMessage(BaseModel):
|
||||
type: str # 'list_reply', 'button_reply', etc.
|
||||
list_reply: Optional[ListReply] = None
|
||||
button_reply: Optional[ButtonMessage] = None # Renomeado para button_reply para consistência
|
||||
|
||||
class Message(BaseModel): # ESTA É A CLASSE 'Message' QUE ESTAVA FALTANDO!
|
||||
from_: str = Field(alias="from") # 'from' é palavra reservada em Python, então usamos alias
|
||||
id: str
|
||||
timestamp: str
|
||||
type: str # 'text', 'image', 'button', 'interactive', etc.
|
||||
text: Optional[TextMessage] = None
|
||||
button: Optional[ButtonMessage] = None # <-- AQUI! Garanta que está usando o ButtonMessage ajustado
|
||||
interactive: Optional[InteractiveMessage] = None
|
||||
# Adicione outros tipos de mensagem aqui se precisar processá-los (image, audio, video, etc.)
|
||||
|
||||
class Status(BaseModel):
|
||||
id: str
|
||||
status: str # 'sent', 'delivered', 'read', 'failed'
|
||||
timestamp: str
|
||||
recipient_id: str
|
||||
# Adicione outros campos de status se necessário
|
||||
|
||||
class ChangeValue(BaseModel):
|
||||
messaging_product: str
|
||||
metadata: dict # Pode conter phone_number_id, display_phone_number
|
||||
messages: Optional[List[Message]] = None
|
||||
statuses: Optional[List[Status]] = None # Para atualizações de status
|
||||
|
||||
class Change(BaseModel):
|
||||
field: str # 'messages' ou 'statuses'
|
||||
value: ChangeValue
|
||||
|
||||
class Entry(BaseModel):
|
||||
id: str
|
||||
changes: List[Change]
|
||||
|
||||
class WebhookEvent(BaseModel): # Este é o seu modelo principal do evento
|
||||
object: str # 'whatsapp_business_account'
|
||||
entry: List[Entry]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,517 @@
|
||||
import httpx
|
||||
from typing import List, Dict, Any, Optional
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
|
||||
|
||||
|
||||
# ... (imports existentes no topo) ...
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio # <-- NOVO: Para agendamento assíncrono
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler # <-- NOVO: Agendador
|
||||
|
||||
|
||||
# --- NOVO: Scheduler para agendar a limpeza de sessões ---
|
||||
scheduler = AsyncIOScheduler()
|
||||
# Iniciar o scheduler quando o aplicativo iniciar (isso será feito no main.py ou api/webhook.py)
|
||||
|
||||
# ... (carregamento de chaves e outras funções) ...
|
||||
|
||||
# --- Variáveis Globais para Gerenciamento de Sessão (APENAS PARA TESTE) ---
|
||||
# Em produção, isso seria um banco de dados
|
||||
ACTIVE_SESSIONS = {} # Dicionário para armazenar sessões ativas
|
||||
SESSION_TIMEOUT_SECONDS = 120 # 5 minutos (5 * 60 segundos)
|
||||
# --- FIM DAS VARIÁVEIS GLOBAIS DE SESSÃO ---
|
||||
|
||||
|
||||
|
||||
# Importações para criptografia
|
||||
from cryptography.hazmat.primitives.asymmetric import padding # <-- Esta linha está correta!
|
||||
from cryptography.hazmat.primitives import hashes # <-- Esta importação está correta!
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
|
||||
from config import WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, VERIFY_TOKEN, FLOW_PRIVATE_KEY_PASSWORD # <-- ADICIONADO
|
||||
# Importe Message para type hinting. Certifique-se que webhook_model.py está correto primeiro!
|
||||
from models.webhook_model import Message, TextMessage, ButtonMessage, InteractiveMessage, ListReply # Adicione os modelos necessários
|
||||
|
||||
FLOW_PRIVATE_KEY_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../private.pem')
|
||||
|
||||
FLOW_PRIVATE_KEY = None # Garante que a variável começa como None antes do try/except
|
||||
|
||||
# --- NOVOS PRINTS PARA DEPURACAO DA CHAVE PRIVADA ---
|
||||
print("\n--- INICIANDO DEPURACAO DE CARREGAMENTO DE CHAVE PRIVADA DO FLOW ---")
|
||||
print(f"DEBUG: Caminho configurado para a chave privada: {FLOW_PRIVATE_KEY_PATH}")
|
||||
|
||||
# Verifica se o arquivo existe antes de tentar abrir
|
||||
if not os.path.exists(FLOW_PRIVATE_KEY_PATH):
|
||||
print(f"DEBUG: ATENÇÃO: Arquivo da chave privada NÃO ENCONTRADO em {FLOW_PRIVATE_KEY_PATH} antes de tentar carregar.")
|
||||
else:
|
||||
print(f"DEBUG: Arquivo da chave privada ENCONTRADO em {FLOW_PRIVATE_KEY_PATH}. Tentando ler...")
|
||||
try:
|
||||
# Tenta ler o conteúdo do arquivo para ter certeza que é acessível e não vazio
|
||||
with open(FLOW_PRIVATE_KEY_PATH, "rb") as key_file_check:
|
||||
key_content_check = key_file_check.read(100) # Lê apenas os primeiros 100 bytes
|
||||
print(f"DEBUG: Primeiros 100 bytes do arquivo da chave (preview): {key_content_check!r}...")
|
||||
if not key_content_check:
|
||||
print("DEBUG: ATENÇÃO: Arquivo da chave privada está VAZIO.")
|
||||
except Exception as e_check:
|
||||
print(f"DEBUG: ERRO ao tentar PRÉ-LER o arquivo da chave: {type(e_check).__name__}: {e_check}")
|
||||
|
||||
print(f"DEBUG: Valor de FLOW_PRIVATE_KEY_PASSWORD (carregado do .env/config): '{FLOW_PRIVATE_KEY_PASSWORD}' (Tipo: {type(FLOW_PRIVATE_KEY_PASSWORD).__name__})")
|
||||
# --- FIM DOS PRINTS DE DEPURACAO DA CHAVE PRIVADA ---
|
||||
|
||||
|
||||
try:
|
||||
with open(FLOW_PRIVATE_KEY_PATH, "rb") as key_file:
|
||||
key_content = key_file.read()
|
||||
|
||||
# CONDIÇÃO CRÍTICA: Use a senha se ela existir, ou None se a chave não tiver senha
|
||||
password_to_use = FLOW_PRIVATE_KEY_PASSWORD.encode('utf-8') if FLOW_PRIVATE_KEY_PASSWORD else None
|
||||
|
||||
print(f"DEBUG: Senha que será usada para carregar a chave: {'(presente)' if password_to_use else '(ausente/None)'}")
|
||||
|
||||
FLOW_PRIVATE_KEY = load_pem_private_key(
|
||||
key_content,
|
||||
password=password_to_use, # <<< ALTERADO PARA USAR A SENHA
|
||||
backend=default_backend()
|
||||
)
|
||||
print(f"✅ Chave privada do Flow carregada de: {FLOW_PRIVATE_KEY_PATH}")
|
||||
except FileNotFoundError:
|
||||
print(f"❌ ERRO: Chave privada do Flow não encontrada em {FLOW_PRIVATE_KEY_PATH}. Verifique o caminho e as permissões.")
|
||||
FLOW_PRIVATE_KEY = None
|
||||
except Exception as e:
|
||||
# A exceção agora provavelmente será relacionada à senha se o arquivo for encontrado
|
||||
# OU se o formato da chave está errado para o password=None/password='suasenha'
|
||||
print(f"❌ ERRO ao carregar chave privada do Flow: {type(e).__name__}: {e}. Verifique se a senha está correta ou se a chave não tem senha. Conteúdo da chave (primeiros 50 bytes): {key_content[:50]!r}...")
|
||||
FLOW_PRIVATE_KEY = None
|
||||
|
||||
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES = """-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsFw96WUer6yyZLQFkUNS
|
||||
Vkv++EEKVVVXca3TcCJqR9aaOepiyKuUT0Il8ctdaZHaTFeEQnM82xbaijEATtGv
|
||||
qmX+zZRnMnRbI63GZm9QM3pnAbs/iXji5PfLbD5AGt8vDUyldlwGu+2e3PHWHPM2
|
||||
MKR/+yHEEmgXWWkCYzgdhwIbsaGQNFdOXykfVpdQLt8237E1VEDEebEP3GlUagvo
|
||||
7k+NQQ+tDHNfAeQjcIHiTCPE39hSFdH7W413HDiu/0sX02ATv+QCppovDBboQdL4
|
||||
OP4nbnvbRn7HHIFIPskpkm5dPfRuDrejb/Q72c1FcuALBAPFpJYCsjUM3JSfa854
|
||||
IwIDAQAB-----END PUBLIC KEY-----""" # <<< COLOQUE A SUA CHAVE PÚBLICA (DO public_flow_key.pem) AQUI! <<<
|
||||
|
||||
try:
|
||||
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES_OBJECT = load_pem_public_key(
|
||||
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES.encode('utf-8'),
|
||||
backend=default_backend()
|
||||
)
|
||||
print("✅ Chave pública da Meta (assumida) para criptografia de retorno (Flows) carregada.")
|
||||
except Exception as e:
|
||||
print(f"❌ ERRO ao carregar chave pública da Meta para Flow (para criptografia de retorno): {e}")
|
||||
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES_OBJECT = None
|
||||
|
||||
def decrypt_flow_request_data(
|
||||
encrypted_flow_data_b64: str,
|
||||
encrypted_aes_key_b64: str,
|
||||
initial_vector_b64: str
|
||||
) -> Dict[str, Any]: # << AQUI ESTÁ A MUDANÇA: AGORA RETORNA UM DICIONÁRIO COMPLETO
|
||||
"""
|
||||
Descriptografa os dados de requisição recebidos de um Flow do WhatsApp.
|
||||
Retorna os dados descriptografados, a chave AES e o IV.
|
||||
"""
|
||||
if not FLOW_PRIVATE_KEY:
|
||||
raise ValueError("Chave privada do Flow não carregada. Não é possível descriptografar a requisição.")
|
||||
|
||||
flow_data = base64.b64decode(encrypted_flow_data_b64)
|
||||
iv = base64.b64decode(initial_vector_b64)
|
||||
|
||||
encrypted_flow_data_body = flow_data[:-16]
|
||||
encrypted_flow_data_tag = flow_data[-16:]
|
||||
|
||||
encrypted_aes_key = base64.b64decode(encrypted_aes_key_b64)
|
||||
aes_key = FLOW_PRIVATE_KEY.decrypt(
|
||||
encrypted_aes_key,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||
algorithm=hashes.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
|
||||
decryptor = Cipher(
|
||||
algorithms.AES(aes_key),
|
||||
modes.GCM(iv, encrypted_flow_data_tag),
|
||||
backend=default_backend()
|
||||
).decryptor()
|
||||
|
||||
decrypted_data_bytes = decryptor.update(encrypted_flow_data_body) + decryptor.finalize()
|
||||
decrypted_data = json.loads(decrypted_data_bytes.decode("utf-8"))
|
||||
|
||||
# Retorna os dados, a chave AES e o IV para uso na resposta
|
||||
return {
|
||||
"decrypted_payload": decrypted_data,
|
||||
"aes_key": aes_key,
|
||||
"initial_vector": iv
|
||||
}
|
||||
|
||||
|
||||
def encrypt_flow_response_data(response_data: Dict[str, Any], aes_key: bytes, initial_vector: bytes) -> str:
|
||||
"""
|
||||
Criptografa os dados de resposta do Flow para enviar ao WhatsApp.
|
||||
Usa a chave AES e o IV (invertido) recebidos na solicitação original.
|
||||
Retorna a string completa codificada em Base64.
|
||||
"""
|
||||
# Nenhuma chave pública RSA é usada AQUI para criptografar a AES_KEY.
|
||||
# A AES_KEY e o IV são passados da requisição original.
|
||||
|
||||
# 1. Preparar initialization vector para resposta (invertendo bits do IV original)
|
||||
flipped_iv = bytearray()
|
||||
for byte in initial_vector:
|
||||
flipped_iv.append(byte ^ 0xFF) # XOR com 0xFF para inverter bits
|
||||
|
||||
# 2. Criptografar o payload de resposta (JSON) usando AES-GCM
|
||||
plaintext_bytes = json.dumps(response_data).encode('utf-8')
|
||||
|
||||
encryptor = Cipher(algorithms.AES(aes_key), modes.GCM(flipped_iv), backend=default_backend()).encryptor()
|
||||
encrypted_data_bytes = encryptor.update(plaintext_bytes) + encryptor.finalize()
|
||||
tag = encryptor.tag # Tag de autenticação do GCM
|
||||
|
||||
# 3. CONSTRUIR A STRING FINAL Base64:
|
||||
# A Meta espera o IV e o TAG concatenados ao final dos dados criptografados,
|
||||
# e então TUDO isso é Base64 codificado. O exemplo Django mostrou isso implicitamente.
|
||||
|
||||
# Payload final em bytes: encrypted_data_bytes + flipped_iv + tag
|
||||
# ATENÇÃO: O exemplo Django concatena o IV no payload final, mas a documentação
|
||||
# da Meta para AES-GCM geralmente não o faz, ela espera que o IV seja separado no cabeçalho.
|
||||
# No entanto, a documentação que você me passou disse "append authentication tag
|
||||
# generated during encryption to the end of the encryption result; encode the whole output as base64 string".
|
||||
#
|
||||
# Vamos seguir o exemplo Django para a resposta. O exemplo Django para `encrypt_response`
|
||||
# retorna base64(encryptor.update(...) + encryptor.finalize() + encryptor.tag).
|
||||
# Ele NÃO inclui a chave AES criptografada aqui, porque a Meta já tem a chave AES.
|
||||
|
||||
# O retorno deve ser SOMENTE o dado criptografado + tag, Base64 codificado.
|
||||
# O IV e a chave AES são gerenciados pela Meta.
|
||||
|
||||
# A ÚLTIMA TENTATIVA DA METAMORA: Retornar a STRING Base64 pura dos dados criptografados + tag
|
||||
# (sem o JSON aninhado que estávamos construindo)
|
||||
|
||||
# A resposta final é apenas a CONCATENAÇÃO dos dados criptografados e o tag, codificado em Base64.
|
||||
final_payload_bytes = encrypted_data_bytes + tag # Dados criptografados + Tag
|
||||
base64_encoded_final_response = base64.b64encode(final_payload_bytes).decode('utf-8')
|
||||
|
||||
return base64_encoded_final_response
|
||||
|
||||
|
||||
|
||||
|
||||
# --- Funções de Gerenciamento de Sessão ---
|
||||
|
||||
def get_session_state(sender_id: str) -> Optional[Dict[str, Any]]:
|
||||
return ACTIVE_SESSIONS.get(sender_id)
|
||||
|
||||
async def clean_session_and_notify(sender_id: str): # <-- FUNÇÃO ASSÍNCRONA PARA LIMPAR E NOTIFICAR
|
||||
"""
|
||||
Função assíncrona que limpa a sessão e envia a mensagem de timeout.
|
||||
Chamada pelo scheduler.
|
||||
"""
|
||||
if sender_id in ACTIVE_SESSIONS: # Verifica se a sessão ainda está ativa (não foi atualizada antes do timeout)
|
||||
timeout_message = "Sua sessão foi encerrada por inatividade. Por favor, envie uma nova mensagem para iniciar um novo atendimento. 😊"
|
||||
print(f"DEBUG_SESSION: Enviando mensagem de timeout para {sender_id}.")
|
||||
await send_text_message(sender_id, timeout_message) # <-- CHAMA A FUNÇÃO DE ENVIO
|
||||
|
||||
del ACTIVE_SESSIONS[sender_id]
|
||||
print(f"DEBUG_SESSION: Sessão para {sender_id} encerrada/limpa pelo agendador.")
|
||||
else:
|
||||
print(f"DEBUG_SESSION: Sessão para {sender_id} já limpa ou atualizada antes do agendamento.")
|
||||
|
||||
def start_or_update_session(sender_id: str):
|
||||
"""
|
||||
Inicia uma nova sessão para o usuário ou atualiza o timestamp da última atividade.
|
||||
Agenda ou reagenda a tarefa de limpeza.
|
||||
"""
|
||||
current_time = datetime.now()
|
||||
ACTIVE_SESSIONS[sender_id] = {
|
||||
"last_activity_time": current_time,
|
||||
"current_state": "INICIO" # Ou qualquer estado inicial padrão
|
||||
}
|
||||
print(f"DEBUG_SESSION: Sessão para {sender_id} iniciada/atualizada em {current_time.strftime('%Y-%m-%d %H:%M:%S')}.")
|
||||
|
||||
# Remover tarefa agendada anterior para esta sessão, se houver
|
||||
job_id = f"session_clean_{sender_id}"
|
||||
if scheduler.get_job(job_id):
|
||||
scheduler.remove_job(job_id)
|
||||
print(f"DEBUG_SESSION: Tarefa de limpeza anterior para {sender_id} cancelada.")
|
||||
|
||||
# Agendar nova tarefa de limpeza para esta sessão
|
||||
scheduler.add_job(
|
||||
clean_session_and_notify,
|
||||
'date', # Agendamento único para uma data/hora específica
|
||||
run_date=current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS),
|
||||
args=[sender_id],
|
||||
id=job_id,
|
||||
replace_existing=True # Para garantir que não haja duplicatas, embora remove_job já ajude
|
||||
)
|
||||
print(f"DEBUG_SESSION: Tarefa de limpeza para {sender_id} agendada para {current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS)}.")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# --- Lógicas de Tratamento de Mensagens Recebidas (Funções Auxiliares) ---
|
||||
# Estas funções contêm a lógica de como seu bot irá interagir.
|
||||
|
||||
# Esta função precisa ser definida no arquivo!
|
||||
async def handle_message_type(message: Message):
|
||||
sender_id = message.from_
|
||||
|
||||
# 1. Iniciar ou atualizar a sessão para o remetente atual
|
||||
# Isso vai automaticamente agendar/reagendar a limpeza.
|
||||
start_or_update_session(sender_id) # Esta função agora agenda a limpeza!
|
||||
|
||||
# 2. Recuperar o estado da sessão (se houver, para uso futuro com grafos)
|
||||
session_data = get_session_state(sender_id)
|
||||
if session_data:
|
||||
print(f"DEBUG_SESSION: Estado atual da sessão para {sender_id}: {session_data['current_state']}")
|
||||
else:
|
||||
# Isso não deveria acontecer se start_or_update_session funcionou
|
||||
print(f"DEBUG_SESSION: Sessão para {sender_id} não encontrada após start_or_update_session (possível erro).")
|
||||
|
||||
if message.type == 'text' and message.text:
|
||||
await handle_text_message(sender_id, message.text.body) # Passe sender_id
|
||||
elif message.type == 'button' and message.button:
|
||||
await handle_button_response(sender_id, message.button.payload) # Passe sender_id
|
||||
elif message.type == 'interactive' and message.interactive:
|
||||
if message.interactive.type == 'list_reply' and message.interactive.list_reply:
|
||||
await handle_list_response(sender_id, message.interactive.list_reply.id, message.interactive.list_reply.title) # Passe sender_id
|
||||
elif message.interactive.type == 'button_reply' and message.interactive.button_reply:
|
||||
await handle_button_response(sender_id, message.interactive.button_reply.payload) # Passe sender_id
|
||||
else:
|
||||
print(f" Tipo interativo desconhecido recebido: {message.interactive.type}")
|
||||
await send_text_message(sender_id, "Desculpe, não entendi essa interação interativa.")
|
||||
elif message.type == 'image':
|
||||
print(' Recebi uma imagem!')
|
||||
await send_text_message(sender_id, "Que legal! Recebi sua imagem. No momento, só consigo processar texto e interações.")
|
||||
else:
|
||||
print(f" Tipo de mensagem não suportado: {message.type}")
|
||||
await send_text_message(sender_id, "Desculpe, não entendi o tipo de mensagem que você enviou.")
|
||||
|
||||
|
||||
async def handle_text_message(sender_id: str, text: str):
|
||||
lower_text = text.lower()
|
||||
if lower_text != "" :
|
||||
await send_text_message(sender_id, "Olá! Você iniciou o Consultme. Escolha uma das opções de consulta no menu abaixo:")
|
||||
await send_interactive_menu(sender_id) # Chama a função para enviar um menu interativo
|
||||
elif 'menu' in lower_text:
|
||||
await send_interactive_menu(sender_id)
|
||||
elif 'cadastro' in lower_text:
|
||||
await send_text_message(sender_id, "Para iniciar seu cadastro, por favor, me diga seu nome completo:")
|
||||
# Implemente lógica para salvar o estado do usuário aqui (ex: em um DB)
|
||||
elif 'ajuda' in lower_text:
|
||||
await send_text_message(sender_id, "Posso te guiar com as funcionalidades principais. Escolha uma opção do menu ou digite uma pergunta.")
|
||||
await send_interactive_menu(sender_id)
|
||||
else:
|
||||
await send_text_message(sender_id, f"Recebi sua mensagem: \"{text}\". Parece que não entendi bem. Você pode digitar 'menu' para ver as opções disponíveis ou 'ajuda'.")
|
||||
|
||||
async def handle_button_response(sender_id: str, payload: str):
|
||||
response_text = ''
|
||||
if payload == 'OPTION_AGENDAR':
|
||||
response_text = "Certo! Para agendar um serviço, qual serviço você precisa e a data/hora preferida?"
|
||||
elif payload == 'OPTION_STATUS':
|
||||
response_text = "Para verificar o status de seu pedido, informe o número do seu pedido."
|
||||
elif payload == 'OPTION_FALAR_ATENDENTE':
|
||||
response_text = "Encaminhando você para um de nossos atendentes. Aguarde, por favor."
|
||||
else:
|
||||
response_text = "Não entendi a opção de botão selecionada. Tente novamente ou digite 'menu'."
|
||||
await send_text_message(sender_id, response_text)
|
||||
|
||||
async def handle_list_response(sender_id: str, list_id: str, list_title: str):
|
||||
response_text = ''
|
||||
if list_id == 'item_reparo_geral':
|
||||
response_text = f"Você selecionou \"{list_title}\". Qual o problema específico que você precisa de reparo?"
|
||||
elif list_id == 'item_instalacao':
|
||||
response_text = f"Você selecionou \"{list_title}\". Qual tipo de instalação você precisa?"
|
||||
elif list_id == 'item_duvidas':
|
||||
response_text = f"Você selecionou \"{list_title}\". Por favor, digite sua pergunta."
|
||||
elif list_id == 'item_reclamacoes':
|
||||
response_text = f"Você selecionou \"{list_title}\". Por favor, descreva o problema em detalhes."
|
||||
else:
|
||||
response_text = "Opção de lista não reconhecida. Tente novamente ou digite 'menu'."
|
||||
await send_text_message(sender_id, response_text)
|
||||
|
||||
# --- Funções de Envio de Mensagens para o WhatsApp ---
|
||||
# Estas funções fazem as chamadas à API da Meta para enviar mensagens.
|
||||
|
||||
async def send_text_message(to: str, text: str):
|
||||
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
payload = {
|
||||
"messaging_product": "whatsapp",
|
||||
"to": to,
|
||||
"type": "text",
|
||||
"text": {
|
||||
"preview_url": False,
|
||||
"body": text
|
||||
}
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
print("✅ Mensagem de texto enviada com sucesso:", response.json())
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"❌ Erro HTTP ao enviar mensagem de texto: {e.response.status_code} - {e.response.text}")
|
||||
except httpx.RequestError as e:
|
||||
print(f"❌ Erro de rede ao enviar mensagem de texto: {e}")
|
||||
|
||||
async def send_interactive_buttons(to: str, header_text: str, body_text: str, buttons_array: List[Dict[str, str]]):
|
||||
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
payload = {
|
||||
"messaging_product": "whatsapp",
|
||||
"to": to,
|
||||
"type": "interactive",
|
||||
"interactive": {
|
||||
"type": "button",
|
||||
"header": { "type": "text", "text": header_text },
|
||||
"body": { "text": body_text },
|
||||
"action": {
|
||||
"buttons": [{"type": "reply", "reply": {"id": btn["id"], "title": btn["title"]}} for btn in buttons_array]
|
||||
}
|
||||
}
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
print("✅ Mensagem com botões enviada com sucesso:", response.json())
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"❌ Erro HTTP ao enviar mensagem com botões: {e.response.status_code} - {e.response.text}")
|
||||
except httpx.RequestError as e:
|
||||
print(f"❌ Erro de rede ao enviar mensagem com botões: {e}")
|
||||
|
||||
async def send_interactive_list(to: str, header_text: str, body_text: str, button_title: str, sections_array: List[Dict[str, Any]]):
|
||||
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
payload = {
|
||||
"messaging_product": "whatsapp",
|
||||
"to": to,
|
||||
"type": "interactive",
|
||||
"interactive": {
|
||||
"type": "list",
|
||||
"header": { "type": "text", "text": header_text },
|
||||
"body": { "text": body_text },
|
||||
"action": {
|
||||
"button": button_title,
|
||||
"sections": [
|
||||
{
|
||||
"title": section["title"],
|
||||
"rows": [{"id": row["id"], "title": row["title"], "description": row.get("description", "")} for row in section["rows"]]
|
||||
} for section in sections_array
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
print("✅ Mensagem de lista enviada com sucesso:", response.json())
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"❌ Erro HTTP ao enviar mensagem de lista: {e.response.status_code} - {e.response.text}")
|
||||
except httpx.RequestError as e:
|
||||
print(f"❌ Erro de rede ao enviar mensagem de lista: {e}")
|
||||
|
||||
async def mark_message_as_read(message_id: str):
|
||||
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
payload = {
|
||||
"messaging_product": "whatsapp",
|
||||
"status": "read",
|
||||
"message_id": message_id
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"❌ Erro HTTP ao marcar mensagem {message_id} como lida: {e.response.status_code} - {e.response.text}")
|
||||
except httpx.RequestError as e:
|
||||
print(f"❌ Erro de rede ao marcar mensagem {message_id} como lida: {e}")
|
||||
|
||||
# --- Função de Exemplo para Enviar um Menu Interativo ---
|
||||
async def send_interactive_menu(to: str):
|
||||
buttons = [
|
||||
{"id": 'OPTION_AGENDAR', "title": 'Total do CP'},
|
||||
{"id": 'OPTION_STATUS', "title": 'Total das suas Lojas'},
|
||||
{"id": 'OPTION_FALAR_ATENDENTE', "title": 'De uma Loja'}
|
||||
]
|
||||
await send_interactive_buttons(to, "Menu Principal", "Escolha a Dimensão das Lojas que você quer visulizar o indicador:", buttons)
|
||||
|
||||
|
||||
async def send_whatsapp_flow(to: str, flow_id: str, flow_cta: str, screen: str = "welcome_screen") -> Dict[str, Any]:
|
||||
url = f"https://graph.facebook.com/v18.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
payload = {
|
||||
"recipient_type": "individual", # <-- ADICIONADO: Conforme a documentação
|
||||
"messaging_product": "whatsapp",
|
||||
"to": to,
|
||||
"type": "interactive",
|
||||
"interactive": {
|
||||
"type": "flow",
|
||||
"header": {
|
||||
"type": "text",
|
||||
"text": "Preencha seu Cadastro"
|
||||
},
|
||||
"body": {
|
||||
"text": "Clique no botão abaixo para preencher o formulário de cadastro."
|
||||
},
|
||||
"footer": {
|
||||
"text": "Consultme - Seu parceiro digital"
|
||||
},
|
||||
"action": {
|
||||
"name": "flow",
|
||||
"parameters": {
|
||||
"flow_id": flow_id, # Usando flow_id (geralmente melhor que flow_name)
|
||||
"flow_cta": flow_cta,
|
||||
# --- ATENÇÃO: MUDAR PARA flow_message_version: "3" (STRING LITERAL "3") ---
|
||||
# Já que o exemplo da documentação é v18.0 com "3", vamos seguir esse exato
|
||||
"flow_message_version": "3" # <--- ALTERE ESTA LINHA PARA "3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ... (restante da função, incluindo DEBUG PRINTS e try/except) ...
|
||||
print(f"DEBUG_FLOW: Payload a ser enviado para o Flow API: {json.dumps(payload, indent=2)}") # Verifique o payload final no log
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
print("✅ Flow enviado com sucesso:", response.json())
|
||||
return {"status": "success", "data": response.json()}
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"❌ ERRO HTTP ao enviar Flow (Status: {e.response.status_code}, Detalhe: {e.response.text}): {e}")
|
||||
# print(f"DEBUG_FLOW_ERROR_RESPONSE: {e.response.json()}") # Descomente para ver o erro JSON completo da Meta
|
||||
return {"status": "error", "message": e.response.text, "code": e.response.status_code}
|
||||
except httpx.RequestError as e:
|
||||
print(f"❌ ERRO de rede ao enviar Flow: {e}")
|
||||
return {"status": "error", "message": str(e), "code": 500}
|
||||
except Exception as e:
|
||||
print(f"❌ ERRO INESPERADO ao enviar Flow: {type(e).__name__}: {e}")
|
||||
return {"status": "error", "message": f"Erro inesperado: {str(e)}", "code": 500}
|
||||
# ... (send_interactive_menu, handle_button_response, etc.) .
|
||||
Reference in New Issue
Block a user