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)
This commit is contained in:
Davidson Gomes 2025-03-06 09:44:59 -03:00
parent 1b66882599
commit b0131a1141
7 changed files with 259 additions and 123 deletions

View File

@ -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: 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 ### 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 ```python
from evolutionapi.services.websocket import WebSocketManager from evolutionapi.services.websocket import WebSocketManager
import logging import logging
@ -889,7 +955,7 @@ logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
) )
# WebSocket configuration # Create WebSocket manager directly
websocket = WebSocketManager( websocket = WebSocketManager(
base_url="http://localhost:8081", # Your Evolution API URL base_url="http://localhost:8081", # Your Evolution API URL
instance_id="test", # Your instance ID instance_id="test", # Your instance ID

View File

@ -9,6 +9,8 @@ from .services.chat import ChatService
from .services.label import LabelService from .services.label import LabelService
from .services.profile import ProfileService from .services.profile import ProfileService
from .services.group import GroupService from .services.group import GroupService
from .services.websocket import WebSocketService, WebSocketManager
class EvolutionClient: class EvolutionClient:
""" """
Cliente para interagir com a API Evolution. Cliente para interagir com a API Evolution.
@ -29,6 +31,7 @@ class EvolutionClient:
self.label = LabelService(self) self.label = LabelService(self)
self.profile = ProfileService(self) self.profile = ProfileService(self)
self.group = GroupService(self) self.group = GroupService(self)
self.websocket = WebSocketService(self)
def _get_headers(self, instance_token: str = None): def _get_headers(self, instance_token: str = None):
return { return {
@ -112,3 +115,24 @@ class EvolutionClient:
url = self._get_full_url(endpoint) url = self._get_full_url(endpoint)
response = requests.delete(url, headers=self._get_headers(instance_token)) response = requests.delete(url, headers=self._get_headers(instance_token))
return self._handle_response(response) return self._handle_response(response)
def create_websocket(self, instance_id: str, api_token: str, max_retries: int = 5, retry_delay: float = 1.0) -> 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
)

View File

@ -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', [])

View File

@ -1,21 +1,60 @@
import socketio import socketio
from typing import Callable, Dict, Any from typing import Callable, Dict, Any
import logging import logging
import ssl
import time import time
from typing import Optional 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: class WebSocketManager:
def __init__(self, base_url: str, instance_id: str, api_token: str, max_retries: int = 5, retry_delay: float = 1.0): 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: Args:
base_url (str): URL base da API base_url (str): Base URL of the API
instance_id (str): ID da instância instance_id (str): Instance ID
api_token (str): Token de autenticação da API api_token (str): API authentication token
max_retries (int): Número máximo de tentativas de reconexão max_retries (int): Maximum number of reconnection attempts
retry_delay (float): Delay inicial entre tentativas em segundos retry_delay (float): Initial delay between attempts in seconds
""" """
self.base_url = base_url.rstrip('/') self.base_url = base_url.rstrip('/')
self.instance_id = instance_id self.instance_id = instance_id
@ -25,85 +64,85 @@ class WebSocketManager:
self.retry_count = 0 self.retry_count = 0
self.should_reconnect = True self.should_reconnect = True
# Configuração do Socket.IO # Socket.IO configuration
self.sio = socketio.Client( self.sio = socketio.Client(
ssl_verify=False, # Para desenvolvimento local ssl_verify=False, # For local development
logger=False, logger=False,
engineio_logger=False, engineio_logger=False,
request_timeout=30 request_timeout=30
) )
# Configura o logger da classe para INFO # Configure class logger to INFO
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.INFO) self.logger.setLevel(logging.INFO)
# Dicionário para armazenar os handlers registrados # Dictionary to store registered handlers
self._handlers = {} self._handlers = {}
# Configuração dos handlers de eventos # Configure event handlers
self.sio.on('connect', self._on_connect) self.sio.on('connect', self._on_connect)
self.sio.on('disconnect', self._on_disconnect) self.sio.on('disconnect', self._on_disconnect)
self.sio.on('error', self._on_error) 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}') self.sio.on('*', self._handle_event, namespace=f'/{self.instance_id}')
def _on_connect(self): def _on_connect(self):
"""Handler para evento de conexão""" """Handler for connection event"""
self.logger.info("Socket.IO conectado") self.logger.info("Socket.IO connected")
self.retry_count = 0 # Reseta o contador de retry após conexão bem-sucedida self.retry_count = 0 # Reset retry counter after successful connection
def _on_disconnect(self): def _on_disconnect(self):
"""Handler para evento de desconexão""" """Handler for disconnection event"""
self.logger.warning(f"Socket.IO desconectado. Tentativa {self.retry_count + 1}/{self.max_retries}") 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: if self.should_reconnect and self.retry_count < self.max_retries:
self._attempt_reconnect() self._attempt_reconnect()
else: 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): def _on_error(self, error):
"""Handler para eventos de erro""" """Handler for error events"""
self.logger.error(f"Erro no Socket.IO: {str(error)}", exc_info=True) self.logger.error(f"Socket.IO error: {str(error)}", exc_info=True)
def _attempt_reconnect(self): def _attempt_reconnect(self):
"""Tenta reconectar com backoff exponencial""" """Attempt to reconnect with exponential backoff"""
try: try:
delay = self.retry_delay * (2 ** self.retry_count) # Backoff exponencial delay = self.retry_delay * (2 ** self.retry_count) # Exponential backoff
self.logger.info(f"Tentando reconectar em {delay:.2f} segundos...") self.logger.info(f"Attempting to reconnect in {delay:.2f} seconds...")
time.sleep(delay) time.sleep(delay)
self.connect() self.connect()
self.retry_count += 1 self.retry_count += 1
except Exception as e: 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: if self.retry_count < self.max_retries:
self._attempt_reconnect() self._attempt_reconnect()
else: else:
self.logger.error("Todas as tentativas de reconexão falharam") self.logger.error("All reconnection attempts failed")
def _handle_event(self, event, *args): def _handle_event(self, event, *args):
"""Handler global para todos os eventos""" """Global handler for all events"""
# Só processa eventos que foram registrados # Only process registered events
if event in self._handlers: if event in self._handlers:
self.logger.debug(f"Evento recebido no namespace /{self.instance_id}: {event}") self.logger.debug(f"Event received in namespace /{self.instance_id}: {event}")
self.logger.debug(f"Dados do evento: {args}") self.logger.debug(f"Event data: {args}")
try: try:
# Extrai os dados do evento # Extract event data
raw_data = args[0] if args else {} 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): 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) self._handlers[event](raw_data)
else: 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: 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): def connect(self):
"""Conecta ao servidor Socket.IO""" """Connect to Socket.IO server"""
try: 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( self.sio.connect(
f"{self.base_url}?apikey={self.api_token}", f"{self.base_url}?apikey={self.api_token}",
transports=['websocket'], transports=['websocket'],
@ -111,25 +150,25 @@ class WebSocketManager:
wait_timeout=30 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}') self.sio.emit('subscribe', {'instance': self.instance_id}, namespace=f'/{self.instance_id}')
except Exception as e: 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 raise
def disconnect(self): def disconnect(self):
"""Desconecta do servidor Socket.IO""" """Disconnect from Socket.IO server"""
self.should_reconnect = False # Impede tentativas de reconexão self.should_reconnect = False # Prevent reconnection attempts
if self.sio.connected: if self.sio.connected:
self.sio.disconnect() self.sio.disconnect()
def on(self, event: str, callback: Callable): def on(self, event: str, callback: Callable):
""" """
Registra um callback para um evento específico Register a callback for a specific event
Args: Args:
event (str): Nome do evento event (str): Event name
callback (Callable): Função a ser chamada quando o evento ocorrer callback (Callable): Function to be called when the event occurs
""" """
self._handlers[event] = callback self._handlers[event] = callback

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name='evolutionapi', name='evolutionapi',
version='0.1.0', version='0.1.1',
description='Client Python para a API Evolution', description='Client Python para a API Evolution',
author='Davidson Gomes', author='Davidson Gomes',
author_email='contato@agenciadgcode.com', author_email='contato@agenciadgcode.com',

View File

@ -1,7 +1,7 @@
from evolutionapi.client import EvolutionClient from evolutionapi.client import EvolutionClient
from evolutionapi.models.instance import InstanceConfig from evolutionapi.models.instance import InstanceConfig
from evolutionapi.models.message import TextMessage, MediaMessage, MediaType from evolutionapi.models.message import TextMessage, MediaMessage, MediaType
from evolutionapi.services.websocket import WebSocketManager from evolutionapi.models.websocket import WebSocketConfig
import time import time
import logging import logging
@ -22,11 +22,54 @@ client = EvolutionClient(
instance_token = "82D55E57CBBC-48A5-98FB-E99655AE7148" instance_token = "82D55E57CBBC-48A5-98FB-E99655AE7148"
instance_id = "teste" instance_id = "teste"
# Inicializando o WebSocket # Configurando eventos do WebSocket
websocket_manager = WebSocketManager( websocket_config = WebSocketConfig(
base_url='http://localhost:8081', 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, instance_id=instance_id,
api_token=instance_token api_token=instance_token,
max_retries=5,
retry_delay=1.0
) )
def on_message(data): def on_message(data):
@ -53,10 +96,24 @@ def on_message(data):
except Exception as e: except Exception as e:
logger.error(f"Erro ao processar mensagem: {e}", exc_info=True) 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...") logger.info("Registrando handlers de eventos...")
# Registrando handlers de eventos # Registrando handlers de eventos
websocket_manager.on('messages.upsert', on_message) websocket_manager.on('messages.upsert', on_message)
websocket_manager.on('qrcode.updated', on_qrcode)
websocket_manager.on('connection.update', on_connection)
try: try:
logger.info("Iniciando conexão WebSocket...") logger.info("Iniciando conexão WebSocket...")
@ -72,11 +129,10 @@ except KeyboardInterrupt:
finally: finally:
websocket_manager.disconnect() websocket_manager.disconnect()
# Exemplos de outras operações (comentados)
# response = client.group.fetch_all_groups(instance_id, instance_token, False) # response = client.group.fetch_all_groups(instance_id, instance_token, False)
# print(response) # print(response)
# text_message = TextMessage( # text_message = TextMessage(
# number="557499879409", # number="557499879409",
# text="Olá, como vai?", # text="Olá, como vai?",
@ -84,7 +140,6 @@ finally:
# ) # )
# response = client.messages.send_text(instance_id, text_message, instance_token) # response = client.messages.send_text(instance_id, text_message, instance_token)
# print("Mensagem de texto enviada") # print("Mensagem de texto enviada")
# print(response) # print(response)
@ -97,73 +152,5 @@ finally:
# ) # )
# response = client.messages.send_media(instance_id, media_message, instance_token, "arquivo.pdf") # response = client.messages.send_media(instance_id, media_message, instance_token, "arquivo.pdf")
# print("Mensagem de mídia enviada") # print("Mensagem de mídia enviada")
# print(response) # 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)