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! # Certifique-se de importar o grafo e o estado inicial from dialog_flow.graph_definition import DIALOG_GRAPH, INITIAL_STATE_ID # Certifique-se de que FlowStatusChangeEvent também está importado se ainda não estiver 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)}.") #Fluxo de mensagens: # --- NOVA FUNÇÃO: O MOTOR DO CHATBOT BASEADO EM GRAFO --- async def process_user_input_with_graph(sender_id: str, message_type: str, message_content: Any): """ Processa a entrada do usuário e avança o estado da conversa no grafo. """ session_data = get_session_state(sender_id) if not session_data: # Se a sessão não existe (timeout ou novo usuário), inicie do estado INICIO current_state_id = INITIAL_STATE_ID start_or_update_session(sender_id) # Garante que a sessão existe session_data = get_session_state(sender_id) # Recarrega para ter certeza else: current_state_id = session_data.get("current_state", INITIAL_STATE_ID) # Pega o estado atual current_state = DIALOG_GRAPH.get(current_state_id) if not current_state: print(f"❌ ERRO GRAVE: Estado '{current_state_id}' não encontrado no DIALOG_GRAPH. Retornando ao INICIO.") current_state_id = INITIAL_STATE_ID current_state = DIALOG_GRAPH[INITIAL_STATE_ID] session_data["current_state"] = INITIAL_STATE_ID # Reseta o estado na sessão print(f"DEBUG_GRAPH: Usuário {sender_id} no estado '{current_state_id}'. Tipo de entrada: {message_type}. Conteúdo: {message_content}") next_state_id = None user_input_processed = str(message_content).lower().strip() # Padroniza a entrada para transições # --- Lógica para determinar o próximo estado (transição) --- if message_type == "text": # Verifica se alguma palavra-chave da transição corresponde ao texto for keyword, target_state in current_state.get("transitions", {}).items(): if keyword != "default" and keyword in user_input_processed: next_state_id = target_state break if not next_state_id: # Se nenhuma palavra-chave específica, usa o default next_state_id = current_state.get("transitions", {}).get("default", INITIAL_STATE_ID) session_data["last_text_input"] = message_content # Salva a última entrada de texto para uso futuro elif message_type == "button_click": # O message_content já é o payload do botão next_state_id = current_state.get("transitions", {}).get(message_content, INITIAL_STATE_ID) elif message_type == "flow_nfm_reply": # Quando uma resposta de Flow chega, o next_state depende do sucesso/falha do Flow # Assumimos que handle_flow_response já processou o Flow e determinou sucesso/falha # A lógica para a transição do Flow precisa ser mais sofisticada. # Por enquanto, vamos para o estado de conclusão do Flow. next_state_id = current_state.get("transitions", {}).get("success", INITIAL_STATE_ID) # Por padrão, vai para o sucesso session_data["flow_data"] = message_content # Salva os dados do Flow para processamento posterior # ... (adicionar outros tipos de entrada, como "api_response" para transições internas, se necessário) ... # Atualiza o estado da sessão session_data["current_state"] = next_state_id start_or_update_session(sender_id) # Atualiza o timestamp da sessão print(f"DEBUG_GRAPH: Usuário {sender_id} transicionou para o estado: '{next_state_id}'") await execute_state_action_and_respond(sender_id, next_state_id, session_data) async def execute_state_action_and_respond(sender_id: str, state_id: str, session_data: Dict[str, Any]): """ Executa a ação de um estado e envia a resposta correspondente. """ state_definition = DIALOG_GRAPH.get(state_id) if not state_definition: print(f"❌ ERRO GRAVE: Definição para o estado '{state_id}' não encontrada. Não é possível responder.") await send_text_message(sender_id, "Ops! Ocorreu um erro interno. Por favor, tente novamente.") session_data["current_state"] = INITIAL_STATE_ID # Reseta o estado start_or_update_session(sender_id) return # --- Ações a serem realizadas pelo bot no novo estado --- if "action_to_perform" in state_definition: action = state_definition["action_to_perform"] print(f"DEBUG_GRAPH: Executando ação para estado '{state_id}': {action}") if action == "send_main_menu": await send_time_menu(sender_id) elif action == "send_main_store": await send_store_menu(sender_id) elif action == "send_flow_cadastro": flow_id = state_definition.get("flow_id") flow_cta = state_definition.get("flow_cta") if flow_id: # O Flow ID real virá da DIALOG_GRAPH, não do SendFlowRequest await send_whatsapp_flow(sender_id, flow_id, flow_cta) else: await send_text_message(sender_id, "Ops! O Flow de cadastro não está configurado. Tente novamente mais tarde.") session_data["current_state"] = INITIAL_STATE_ID start_or_update_session(sender_id) elif action == "process_cadastro_data": # Aqui você processaria os dados do Flow que estão em session_data["flow_data"] flow_data = json.loads(session_data.get("flow_data", "{}")) # Carregar os dados salvos nome_completo = flow_data.get("nome_completo") email = flow_data.get("email") if nome_completo and email: print(f"DEBUG_GRAPH: Processando cadastro para {nome_completo}, {email}") # AQUI você salvaria no BD/CRM # salvar_usuario_no_banco_de_dados(sender_id, nome_completo, email) # enviar_para_crm(nome_completo, email) response_message = state_definition["message"].replace("${nome_completo}", nome_completo).replace("${email}", email) await send_text_message(sender_id, response_message) else: await send_text_message(sender_id, "Falha ao processar cadastro. Dados incompletos. Por favor, tente novamente.") session_data["current_state"] = DIALOG_GRAPH.get("CADASTRO_FALHA", {}).get("transitions",{}).get("default", INITIAL_STATE_ID) # Ir para estado de falha start_or_update_session(sender_id) elif action == "save_temp_service": # Exemplo: guardar o serviço agendado # service = session_data.get("last_text_input") # Pega o último texto que o usuário enviou # session_data["servico_agendado"] = service # start_or_update_session(sender_id) # Atualiza a sessão pass # Ação interna, não envia mensagem aqui elif action == "confirm_appointment": # Exemplo: confirmar agendamento no sistema externo # (Chamada de API para sistema de agendamento) pass # Ação interna, a mensagem já vem do state_definition elif action == "call_external_status_api": # AQUI é onde você faria a chamada para o seu sistema externo # Por enquanto, apenas um placeholder # numero_pedido = session_data.get("last_text_input") # print(f"DEBUG_GRAPH: Chamando API externa para pedido {numero_pedido}") # try: # status_api = await external_api_call(numero_pedido) # session_data["status_retornado"] = status_api # Salva na sessão # # Transicionar internamente para STATUS_EXIBIR ou STATUS_NAO_ENCONTRADO # await execute_state_action_and_respond(sender_id, "STATUS_EXIBIR", session_data) # Transição interna # return # except Exception: # await execute_state_action_and_respond(sender_id, "STATUS_NAO_ENCONTRADO", session_data) # Transição interna # return pass # Implementação futura, a mensagem vem do estado. # --- Enviar a mensagem do novo estado (se houver) --- if "message" in state_definition: # Substitui placeholders na mensagem final_message = state_definition["message"] if "${nome_completo}" in final_message and session_data.get("flow_data"): flow_data = json.loads(session_data["flow_data"]) final_message = final_message.replace("${nome_completo}", flow_data.get("nome_completo", "usuário")) if "${email}" in final_message and session_data.get("flow_data"): flow_data = json.loads(session_data["flow_data"]) final_message = final_message.replace("${email}", flow_data.get("email", "não informado")) # Para outros placeholders como ${servico_agendado}, ${data_horario_agendado}, ${numero_pedido}, ${status_retornado} # você faria substituições semelhantes baseadas em session_data await send_text_message(sender_id, final_message) # Lógica para limpar a sessão se o estado for terminal if state_definition.get("terminal"): await clean_session_and_notify(sender_id, send_timeout_message=False) # Não envia msg de timeout print(f"DEBUG_GRAPH: Sessão para {sender_id} encerrada no estado terminal '{state_id}'.") # --- Lógicas de Tratamento de Mensagens Recebidas (Funções Auxiliares) --- # Estas funções contêm a lógica de como seu bot irá interagir. # --- handle_message_type: Agora é o ponto de entrada principal que chama o motor do grafo --- #--- FUNÇÃO handle_message_type: A VERSÃO CORRETA PARA O TIMEOUT E GRAFO --- async def handle_message_type(message: Message): sender_id = message.from_ # 1. Limpar sessões inativas # Esta função limpa as sessões que expiraram e envia a mensagem de timeout via scheduler. start_or_update_session(sender_id) # Esta é a que precisa ser assíncrona # 2. INICIAR OU ATUALIZAR A SESSÃO PARA O REMETENTE ATUAL. # ISSO É ESSENCIAL PARA O TIMEOUT. Se o usuário mandar mensagem, a sessão dele é atualizada # e o agendamento de timeout é resetado. start_or_update_session(sender_id) # 3. Recuperar o estado da sessão (já atualizado) 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: ERRO: Sessão para {sender_id} não encontrada após atualização. Isso é inesperado.") # Se por algum motivo não tiver sessão, podemos resetar para o estado inicial start_or_update_session(sender_id) session_data = get_session_state(sender_id) # Tenta novamente message_content = None message_type = None # --- IDENTIFICAÇÃO DO TIPO DE MENSAGEM --- # Esta parte permanece como estava, identificando o tipo e conteúdo. if message.type == 'text' and message.text: message_type = "text" message_content = message.text.body elif message.type == 'button' and message.button: # Resposta de botão de resposta rápida message_type = "button_click" message_content = message.button.payload elif message.type == 'interactive' and message.interactive: if message.interactive.type == 'list_reply' and message.interactive.list_reply: # Resposta de lista message_type = "list_reply" message_content = message.interactive.list_reply.id elif message.interactive.type == 'button_reply' and message.interactive.button_reply: # Resposta de botão interativo message_type = "button_click" # Tratar como clique de botão message_content = message.interactive.button_reply.id elif message.interactive.type == 'nfm_reply' and message.interactive.nfm_reply: # Resposta de Flow message_type = "flow_nfm_reply" message_content = message.interactive.nfm_reply.response_json else: print(f" Tipo interativo desconhecido recebido: {message.interactive.type}") await send_text_message(sender_id, "Desculpe, não entendi essa interação interativa.") return # Sai, não há transição no grafo para isso elif message.type == 'image': # Mensagens que não são processadas pelo grafo message_type = "image" message_content = "imagem_recebida" # Placeholder await send_text_message(sender_id, "Recebi sua imagem. No momento, só consigo processar texto e interações.") return # Sai, não há transição no grafo para isso else: # Tipo de mensagem não suportado ou desconhecido message_type = "unsupported" message_content = "tipo_desconhecido" await send_text_message(sender_id, "Desculpe, não entendi o tipo de mensagem que você enviou.") return # Sai # 4. O motor do grafo processa a entrada e gerencia o estado da sessão. # A `session_data` já foi atualizada no passo 2. await process_user_input_with_graph(sender_id, message_type, message_content) # --- 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 --- # --- NOVA FUNÇÃO: send_stores_list_menu (Para menu de LISTA de lojas) --- async def send_time_menu(to: str): # <-- FUNÇÃO QUE VOCÊ PEDIU """ Envia uma mensagem interativa de LISTA para o usuário com opções de lojas. """ header_text = "Escolha o Período que você quer visualizar o indicador" body_text = "Veja as opções a baixo" button_title = "Clique aqui" # Texto do botão que o usuário clica para ABRIR a lista sections = [ { "title": "Acumulados", # Título da primeira seção "rows": [ {"id": 'OPTION_ANO', "title": 'Acumulado do Ano', "description": "Do do ano até ontem."}, {"id": 'OPTION_MES', "title": 'Acumulado do Mês', "description": "Do primeira do mês até ontem."}, ] }, { "title": "Resultados", # Título da segunda seção "rows": [ {"id": 'OPTION_ONTEM', "title": 'Resultado de Ontem', "description": "Ver o realizado de ontem."}, {"id": 'OPTION_HOJE', "title": 'Resultado de Hoje', "description": "Resultado Parcial de Hoje"} ] }, { "title": "Sair", # Título da segunda seção "rows": [ {"id": 'OPTION_SAIR', "title": 'Sair', "description": "Encerre a conversa"}, ] } ] # Relembrando: até 10 seções e 10 itens por seção (total 100 itens). # CHAMA send_interactive_list DIRETAMENTE AQUI await send_interactive_list(to, header_text, body_text, button_title, sections) async def send_store_menu(to: str): # <-- FUNÇÃO QUE VOCÊ PEDIU """ Envia uma mensagem interativa de LISTA para o usuário com opções de lojas. """ header_text = "Escolha o Período que você quer visualizar o indicador" body_text = "Veja as opções a baixo" button_title = "Clique aqui" # Texto do botão que o usuário clica para ABRIR a lista sections = [ { "title": "Acumulados", # Título da primeira seção "rows": [ {"id": 'OPTION_ANO', "title": 'Total do CP', "description": "Visualize o resultado Total do CP"}, {"id": 'OPTION_MES', "title": 'Total do Estado', "description": "Visualize o resultado Total das suas Lojas"}, ] }, { "title": "Por Loja", # Título da segunda seção "rows": [ {"id": 'OPTION_ONTEM', "title": 'Total das suas Lojas', "description": "Visualize o total das suas Lojas"}, {"id": 'OPTION_HOJE', "title": 'Total de uma Loja', "description": "Visualize o total de uma loja."} ] }, { "title": "Sair", # Título da segunda seção "rows": [ {"id": 'OPTION_SAIR', "title": 'Sair', "description": "Encerre a conversa"}, ] } ] # Relembrando: até 10 seções e 10 itens por seção (total 100 itens). # CHAMA send_interactive_list DIRETAMENTE AQUI await send_interactive_list(to, header_text, body_text, button_title, sections) # Exemplo de como usar o MENU, só aceita 3 opções mas já aparece direto pro usuário as opções. ''' async def send_stores_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_time_menu(to: str): buttons = [ {"id": 'OPTION_AGENDAR', "title": 'Acumulado do Ano'}, {"id": 'OPTION_STATUS', "title": 'Acumulado do Mês'}, {"id": 'OPTION_FALAR_ATENDENTE', "title": 'Resultado de Ontem'} #{"id": 'OPTION_FALAR_ATENDENTE', "title": 'Resultado Parcial de Hoje'}, #{"id": 'OPTION_FALAR_ATENDENTE', "title": 'Sair'} ] await send_interactive_buttons(to, "Escolha o Período que você quer visualizar o indicador", "Veja as opções abaixo:", 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.) .