From b0131a1141ca89ee4d8c35b472546105eec4ab81 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 6 Mar 2025 09:44:59 -0300 Subject: [PATCH] feat: Add full support for WebSocket configuration and management in the Evolution API Client library - Implement WebSocketConfig and WebSocketInfo models for event configuration - Add create_websocket() method in the client for simplified creation of WebSocket handlers - Expand WebSocket service with set_websocket() and find_websocket() methods - Update README with detailed documentation on WebSocket configuration and usage - Add new event handlers in the test example (on_qrcode, on_connection) --- README.md | 68 ++++++++- .../__pycache__/client.cpython-310.pyc | Bin 3821 -> 4697 bytes evolutionapi/client.py | 24 +++ evolutionapi/models/websocket.py | 20 +++ evolutionapi/services/websocket.py | 127 ++++++++++------ setup.py | 2 +- test_evolution.py | 141 ++++++++---------- 7 files changed, 259 insertions(+), 123 deletions(-) create mode 100644 evolutionapi/models/websocket.py diff --git a/README.md b/README.md index fb24e6e..fa7bd00 100644 --- a/README.md +++ b/README.md @@ -877,8 +877,74 @@ response = client.label.handle_label(instance_id, config, instance_token) The Evolution API client supports WebSocket connection to receive real-time events. Here's a guide on how to use it: +### Prerequisites + +Before using WebSocket, you need to: + +1. Enable WebSocket in your Evolution API by setting the environment variable: +```bash +WEBSOCKET_ENABLED=true +``` + +2. Configure WebSocket events for your instance using the WebSocket service: +```python +from evolutionapi.models.websocket import WebSocketConfig + +# Configure WebSocket events +config = WebSocketConfig( + enabled=True, + events=[ + "APPLICATION_STARTUP", + "QRCODE_UPDATED", + "MESSAGES_SET", + "MESSAGES_UPSERT", + "MESSAGES_UPDATE", + "MESSAGES_DELETE", + "SEND_MESSAGE", + "CONTACTS_SET", + "CONTACTS_UPSERT", + "CONTACTS_UPDATE", + "PRESENCE_UPDATE", + "CHATS_SET", + "CHATS_UPSERT", + "CHATS_UPDATE", + "CHATS_DELETE", + "GROUPS_UPSERT", + "GROUP_UPDATE", + "GROUP_PARTICIPANTS_UPDATE", + "CONNECTION_UPDATE", + "LABELS_EDIT", + "LABELS_ASSOCIATION", + "CALL", + "TYPEBOT_START", + "TYPEBOT_CHANGE_STATUS" + ] +) + +# Set WebSocket configuration +response = client.websocket.set_websocket(instance_id, config, instance_token) + +# Get current WebSocket configuration +websocket_info = client.websocket.find_websocket(instance_id, instance_token) +print(f"WebSocket enabled: {websocket_info.enabled}") +print(f"Configured events: {websocket_info.events}") +``` + ### Basic Configuration +There are two ways to create a WebSocket manager: + +1. Using the client's helper method: +```python +# Create WebSocket manager using the client +websocket = client.create_websocket( + instance_id="test", + max_retries=5, # Maximum number of reconnection attempts + retry_delay=1.0 # Initial delay between attempts in seconds +) +``` + +2. Creating the manager directly: ```python from evolutionapi.services.websocket import WebSocketManager import logging @@ -889,7 +955,7 @@ logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) -# WebSocket configuration +# Create WebSocket manager directly websocket = WebSocketManager( base_url="http://localhost:8081", # Your Evolution API URL instance_id="test", # Your instance ID diff --git a/evolutionapi/__pycache__/client.cpython-310.pyc b/evolutionapi/__pycache__/client.cpython-310.pyc index 5834b2515125725301dbc3ce8ec947582bb8d321..2a9c6155b73c9b22b12936b8c948602a9235c7f2 100644 GIT binary patch delta 1961 zcmZuy&2Jk;6yI5|HyhjQIF9qxre%_vBnxVw0$ND=6{P_p8n+}=NakQRo^i5dz3a}d zlcuudLwy4d(OwYTk-`Zn7Y_Ub@E@=@PKW~%NSuHJZ)WXHY-Ox@e)HzN-@Nx`-rGN} z{8G%ix}K6?Y<%_8?rGsTJA=MEe)aPrNf6zhtWTAv>eJE`vn^<_yh}D|DnluF_mBMP4Ep6h?^((G^b*WF4VK&`Hpj{jrwx z0=1+{fk@lZiCp>zy=IJuD)W41HCP@ZVPk=!9L=*4;KSiVy5otI7>Q8vs9o#oz~+g> zW`jU^lAYx7PXNgv6{Ui|m9Lk^U}eY*!yhYhG8>aQ;KEF0m->}GY7)x4V(??~la&I< z4J6>ZmZ3%+(r_)uA2ip6d97*Nl_s-aIo(n_=q~2K8lzsrbvznoC}S?G5bB$jJqVDi z?3oU+X{D!rJt$}&pvB-*?QqlR0zGX~&-cOvxXKnl#@+yOl8mBSc-WZoV^X?L}r4&4!n_x^12XwlUliX3nZb^aN$zKrhe{7 zZW*QY4(FCR5is|IOnPDZB0t^-o%P^h`t;E%a7L2x0GT)>(P;XSIgbstFy9ifiGu|E zMVOnQw;z-;chH-`7nz0jTao!0vP!5;eR`HS_6~<0NKpx7Pq};r$n%Ny1J`c)mg}t9 z7Iplk2;=u8Z2krl#-I1XwB>ld=~QXOB4N62K7&ha!}FzHm}YQQM{wu1c?6G|`c39k zxpi-FehB6oOpm`fUusF8p%!Y%twc*9=*uLOjBI|3Bq@cvV~tVMr`W`Cf57#q9pIYF zu)jxf+iTFORkJ9;J@1xc`~bsNk0BY^9P@y3Hbb+chre0QHZ z=ap9+%{q3P^&QHvTf>Z2UB{tSo^x#aKCL%=uMg&2 zM)6|Jc1;en;aI+9+E^qS?@+&eNWrNd0QiqRj!gQoV&E-tGXuGZv)k>pQ7U3-obz6C zVl;TRZPB?tgbQV3oNRHj4FpX=W6A{)E&Yjhi;CO>&qd>U3qAzQbg7R8U*HkT0$j_*$`GQ(hnZ}ij%j{P0 ygZ>u@^Ir7T^OoYM#YZ4IiR)f;QmEaoyQFE;yHGNccL|6FAA$nP4E%B^FaHm2jKzEa delta 991 zcmZuvOK;Oa5cb-RaS}VP=1CrlUIamiF`P?9BJg%^^SSw>z&Ikq9sr*b$# zRD6l3wu+-THVia__%iFr`K26A;q(wS4Llu_!C8ngu?8~v$yeD#$Y!2G@>trJ#&Y8q zx@OIJ8tuEZ-J>y(qj84SS7}S})IM?SGanp`C0b%i;Hi<~MI`|Yq%k{iQjLRc$+QSR zy1=3I59Y!0jMfoxTgT)XCbYl6s@lP30{j*d841x2aeMkP^U8a zq74x$vPXI$JSD_>dJ3(w_xi574CKr(a_pym@MMKopi}ZBXqQ=k-D1|Pi+Fb?=t z^9)hCr+@^l^6eBi-`)nOO+Ez|kv7NoOYAhXR=UZ}djMmZ3VRi7%kZaGW@dPE{T6sU zwbz9ai$iltl4trO+u+t64uYo6-h?Z;S#|6{%ma2Bp6qRbWd$g2kO~o{H*o!Q-Wwc= z8xg$;AwfTj^D%@Uv(H8yt+78w#lCToS;NF3F8Mz=y3SQEjA$2p9qzvi@H)Novg-`o zcDJ+bv`NRU`WzN&SW2VOup(?PR(-q8 WebSocketManager: + """ + Create a WebSocket manager for the specified instance. + + Args: + instance_id (str): The instance ID + api_token (str): The API token + max_retries (int): Maximum number of reconnection attempts + retry_delay (float): Initial delay between attempts in seconds + + Returns: + WebSocketManager: The WebSocket manager instance + """ + return WebSocketManager( + base_url=self.base_url, + instance_id=instance_id, + api_token=api_token, + max_retries=max_retries, + retry_delay=retry_delay + ) diff --git a/evolutionapi/models/websocket.py b/evolutionapi/models/websocket.py new file mode 100644 index 0000000..56fbea3 --- /dev/null +++ b/evolutionapi/models/websocket.py @@ -0,0 +1,20 @@ +from typing import List, Optional +from dataclasses import dataclass + +@dataclass +class WebSocketConfig: + enabled: bool + events: List[str] + + def __init__(self, enabled: bool, events: List[str]): + self.enabled = enabled + self.events = events + +@dataclass +class WebSocketInfo: + enabled: bool + events: List[str] + + def __init__(self, **kwargs): + self.enabled = kwargs.get('enabled', False) + self.events = kwargs.get('events', []) \ No newline at end of file diff --git a/evolutionapi/services/websocket.py b/evolutionapi/services/websocket.py index d3e89af..6839881 100644 --- a/evolutionapi/services/websocket.py +++ b/evolutionapi/services/websocket.py @@ -1,21 +1,60 @@ import socketio from typing import Callable, Dict, Any import logging -import ssl import time from typing import Optional +from ..models.websocket import WebSocketConfig, WebSocketInfo + +class WebSocketService: + def __init__(self, client): + self.client = client + + def set_websocket(self, instance_id: str, config: WebSocketConfig, instance_token: str): + """ + Configure WebSocket settings for an instance + + Args: + instance_id (str): The instance ID + config (WebSocketConfig): The WebSocket configuration + instance_token (str): The instance token + + Returns: + dict: The response from the API + """ + return self.client.post( + f'websocket/set/{instance_id}', + data=config.__dict__, + instance_token=instance_token + ) + + def find_websocket(self, instance_id: str, instance_token: str) -> WebSocketInfo: + """ + Get WebSocket settings for an instance + + Args: + instance_id (str): The instance ID + instance_token (str): The instance token + + Returns: + WebSocketInfo: The WebSocket information + """ + response = self.client.get( + f'websocket/find/{instance_id}', + instance_token=instance_token + ) + return WebSocketInfo(**response) class WebSocketManager: def __init__(self, base_url: str, instance_id: str, api_token: str, max_retries: int = 5, retry_delay: float = 1.0): """ - Inicializa o gerenciador de WebSocket + Initialize the WebSocket manager Args: - base_url (str): URL base da API - instance_id (str): ID da instância - api_token (str): Token de autenticação da API - max_retries (int): Número máximo de tentativas de reconexão - retry_delay (float): Delay inicial entre tentativas em segundos + base_url (str): Base URL of the API + instance_id (str): Instance ID + api_token (str): API authentication token + max_retries (int): Maximum number of reconnection attempts + retry_delay (float): Initial delay between attempts in seconds """ self.base_url = base_url.rstrip('/') self.instance_id = instance_id @@ -25,85 +64,85 @@ class WebSocketManager: self.retry_count = 0 self.should_reconnect = True - # Configuração do Socket.IO + # Socket.IO configuration self.sio = socketio.Client( - ssl_verify=False, # Para desenvolvimento local + ssl_verify=False, # For local development logger=False, engineio_logger=False, request_timeout=30 ) - # Configura o logger da classe para INFO + # Configure class logger to INFO self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.INFO) - # Dicionário para armazenar os handlers registrados + # Dictionary to store registered handlers self._handlers = {} - # Configuração dos handlers de eventos + # Configure event handlers self.sio.on('connect', self._on_connect) self.sio.on('disconnect', self._on_disconnect) self.sio.on('error', self._on_error) - # Registra o handler global no namespace específico da instância + # Register global handler in instance-specific namespace self.sio.on('*', self._handle_event, namespace=f'/{self.instance_id}') def _on_connect(self): - """Handler para evento de conexão""" - self.logger.info("Socket.IO conectado") - self.retry_count = 0 # Reseta o contador de retry após conexão bem-sucedida + """Handler for connection event""" + self.logger.info("Socket.IO connected") + self.retry_count = 0 # Reset retry counter after successful connection def _on_disconnect(self): - """Handler para evento de desconexão""" - self.logger.warning(f"Socket.IO desconectado. Tentativa {self.retry_count + 1}/{self.max_retries}") + """Handler for disconnection event""" + self.logger.warning(f"Socket.IO disconnected. Attempt {self.retry_count + 1}/{self.max_retries}") if self.should_reconnect and self.retry_count < self.max_retries: self._attempt_reconnect() else: - self.logger.error("Número máximo de tentativas de reconexão atingido") + self.logger.error("Maximum number of reconnection attempts reached") def _on_error(self, error): - """Handler para eventos de erro""" - self.logger.error(f"Erro no Socket.IO: {str(error)}", exc_info=True) + """Handler for error events""" + self.logger.error(f"Socket.IO error: {str(error)}", exc_info=True) def _attempt_reconnect(self): - """Tenta reconectar com backoff exponencial""" + """Attempt to reconnect with exponential backoff""" try: - delay = self.retry_delay * (2 ** self.retry_count) # Backoff exponencial - self.logger.info(f"Tentando reconectar em {delay:.2f} segundos...") + delay = self.retry_delay * (2 ** self.retry_count) # Exponential backoff + self.logger.info(f"Attempting to reconnect in {delay:.2f} seconds...") time.sleep(delay) self.connect() self.retry_count += 1 except Exception as e: - self.logger.error(f"Erro durante tentativa de reconexão: {str(e)}", exc_info=True) + self.logger.error(f"Error during reconnection attempt: {str(e)}", exc_info=True) if self.retry_count < self.max_retries: self._attempt_reconnect() else: - self.logger.error("Todas as tentativas de reconexão falharam") + self.logger.error("All reconnection attempts failed") def _handle_event(self, event, *args): - """Handler global para todos os eventos""" - # Só processa eventos que foram registrados + """Global handler for all events""" + # Only process registered events if event in self._handlers: - self.logger.debug(f"Evento recebido no namespace /{self.instance_id}: {event}") - self.logger.debug(f"Dados do evento: {args}") + self.logger.debug(f"Event received in namespace /{self.instance_id}: {event}") + self.logger.debug(f"Event data: {args}") try: - # Extrai os dados do evento + # Extract event data raw_data = args[0] if args else {} - # Garante que estamos passando o objeto correto para o callback + # Ensure we're passing the correct object to the callback if isinstance(raw_data, dict): - self.logger.debug(f"Chamando handler para {event} com dados: {raw_data}") + self.logger.debug(f"Calling handler for {event} with data: {raw_data}") self._handlers[event](raw_data) else: - self.logger.error(f"Dados inválidos recebidos para evento {event}: {raw_data}") + self.logger.error(f"Invalid data received for event {event}: {raw_data}") except Exception as e: - self.logger.error(f"Erro ao processar evento {event}: {str(e)}", exc_info=True) + self.logger.error(f"Error processing event {event}: {str(e)}", exc_info=True) def connect(self): - """Conecta ao servidor Socket.IO""" + """Connect to Socket.IO server""" try: - # Conecta apenas ao namespace da instância com o header de autenticação + # Connect only to instance namespace with authentication header self.sio.connect( f"{self.base_url}?apikey={self.api_token}", transports=['websocket'], @@ -111,25 +150,25 @@ class WebSocketManager: wait_timeout=30 ) - # Entra na sala específica da instância + # Join instance-specific room self.sio.emit('subscribe', {'instance': self.instance_id}, namespace=f'/{self.instance_id}') except Exception as e: - self.logger.error(f"Erro ao conectar ao Socket.IO: {str(e)}", exc_info=True) + self.logger.error(f"Error connecting to Socket.IO: {str(e)}", exc_info=True) raise def disconnect(self): - """Desconecta do servidor Socket.IO""" - self.should_reconnect = False # Impede tentativas de reconexão + """Disconnect from Socket.IO server""" + self.should_reconnect = False # Prevent reconnection attempts if self.sio.connected: self.sio.disconnect() def on(self, event: str, callback: Callable): """ - Registra um callback para um evento específico + Register a callback for a specific event Args: - event (str): Nome do evento - callback (Callable): Função a ser chamada quando o evento ocorrer + event (str): Event name + callback (Callable): Function to be called when the event occurs """ self._handlers[event] = callback \ No newline at end of file diff --git a/setup.py b/setup.py index 2d9cb1a..85d94d9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='evolutionapi', - version='0.1.0', + version='0.1.1', description='Client Python para a API Evolution', author='Davidson Gomes', author_email='contato@agenciadgcode.com', diff --git a/test_evolution.py b/test_evolution.py index 10f3a0c..954bd88 100644 --- a/test_evolution.py +++ b/test_evolution.py @@ -1,7 +1,7 @@ from evolutionapi.client import EvolutionClient from evolutionapi.models.instance import InstanceConfig from evolutionapi.models.message import TextMessage, MediaMessage, MediaType -from evolutionapi.services.websocket import WebSocketManager +from evolutionapi.models.websocket import WebSocketConfig import time import logging @@ -22,11 +22,54 @@ client = EvolutionClient( instance_token = "82D55E57CBBC-48A5-98FB-E99655AE7148" instance_id = "teste" -# Inicializando o WebSocket -websocket_manager = WebSocketManager( - base_url='http://localhost:8081', +# Configurando eventos do WebSocket +websocket_config = WebSocketConfig( + enabled=True, + events=[ + "APPLICATION_STARTUP", + "QRCODE_UPDATED", + "MESSAGES_SET", + "MESSAGES_UPSERT", + "MESSAGES_UPDATE", + "MESSAGES_DELETE", + "SEND_MESSAGE", + "CONTACTS_SET", + "CONTACTS_UPSERT", + "CONTACTS_UPDATE", + "PRESENCE_UPDATE", + "CHATS_SET", + "CHATS_UPSERT", + "CHATS_UPDATE", + "CHATS_DELETE", + "GROUPS_UPSERT", + "GROUP_UPDATE", + "GROUP_PARTICIPANTS_UPDATE", + "CONNECTION_UPDATE", + "LABELS_EDIT", + "LABELS_ASSOCIATION", + "CALL", + "TYPEBOT_START", + "TYPEBOT_CHANGE_STATUS" + ] +) + +# Configurando WebSocket para a instância +logger.info("Configurando WebSocket...") +response = client.websocket.set_websocket(instance_id, websocket_config, instance_token) +logger.info(f"Configuração WebSocket: {response}") + +# Obtendo configuração atual do WebSocket +websocket_info = client.websocket.find_websocket(instance_id, instance_token) +logger.info(f"WebSocket habilitado: {websocket_info.enabled}") +logger.info(f"Eventos configurados: {websocket_info.events}") + +# Criando gerenciador WebSocket usando o cliente +logger.info("Criando gerenciador WebSocket...") +websocket_manager = client.create_websocket( instance_id=instance_id, - api_token=instance_token + api_token=instance_token, + max_retries=5, + retry_delay=1.0 ) def on_message(data): @@ -53,10 +96,24 @@ def on_message(data): except Exception as e: logger.error(f"Erro ao processar mensagem: {e}", exc_info=True) +def on_qrcode(data): + """Handler para evento de QR Code""" + logger.info("=== QR Code Atualizado ===") + logger.info(f"QR Code: {data}") + logger.info("=======================") + +def on_connection(data): + """Handler para evento de conexão""" + logger.info("=== Status de Conexão ===") + logger.info(f"Status: {data}") + logger.info("=======================") + logger.info("Registrando handlers de eventos...") # Registrando handlers de eventos websocket_manager.on('messages.upsert', on_message) +websocket_manager.on('qrcode.updated', on_qrcode) +websocket_manager.on('connection.update', on_connection) try: logger.info("Iniciando conexão WebSocket...") @@ -72,11 +129,10 @@ except KeyboardInterrupt: finally: websocket_manager.disconnect() +# Exemplos de outras operações (comentados) # response = client.group.fetch_all_groups(instance_id, instance_token, False) - # print(response) - # text_message = TextMessage( # number="557499879409", # text="Olá, como vai?", @@ -84,7 +140,6 @@ finally: # ) # response = client.messages.send_text(instance_id, text_message, instance_token) - # print("Mensagem de texto enviada") # print(response) @@ -97,73 +152,5 @@ finally: # ) # response = client.messages.send_media(instance_id, media_message, instance_token, "arquivo.pdf") - # print("Mensagem de mídia enviada") -# print(response) - -# print("Buscando instâncias") -# instances = client.instances.fetch_instances() - -# print("Instâncias encontradas") -# print(instances) - -# print("Criando instância") -# config = InstanceConfig( -# instanceName="instance-python3", -# integration="WHATSAPP-BAILEYS", -# qrcode=True, -# ) - -# new_instance = client.instances.create_instance(config) - -# print("Instância criada") -# print(new_instance) - -# instance_token = new_instance['hash'] -# instance_id = new_instance['instance']['instanceName'] - -# print("Recuperando estado de conexão") -# connection_state = client.instance_operations.get_connection_state(instance_id, instance_token) - -# print("Estado de conexão") -# print(connection_state) - -# print("Conectando instância") -# connection_state = client.instance_operations.connect(instance_id, instance_token) - -# print("Estado de conexão") -# print(connection_state) - -# print("Reiniciando instância") -# restart_instance = client.instance_operations.restart(instance_id, instance_token) - -# print("Instância reiniciada") -# print(restart_instance) - -# print("Desconectando instância") -# logout_instance = client.instance_operations.logout(instance_id, instance_token) - -# print("Instância desconectada") -# print(logout_instance) - -# print("Deletando instância") -# delete_instance = client.instance_operations.delete(instance_id, instance_token) - -# print("Instância deletada") -# print(delete_instance) - -# group_id = "120363026465248932@g.us" - -# # Buscando as 3 últimas mensagens do grupo -# mensagens = client.chat.get_messages( -# instance_id=instance_id, -# remote_jid=group_id, -# instance_token=instance_token, -# timestamp_start="2025-01-16T00:00:00Z", -# timestamp_end="2025-01-16T23:59:59Z", -# page=1, -# offset=10 -# ) - -# print("Mensagens encontradas:") -# print(mensagens) \ No newline at end of file +# print(response) \ No newline at end of file