whatsapp-toolkit 1.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- whatsapp_toolkit-1.0.6/PKG-INFO +113 -0
- whatsapp_toolkit-1.0.6/README.md +90 -0
- whatsapp_toolkit-1.0.6/pyproject.toml +34 -0
- whatsapp_toolkit-1.0.6/src/whatsapp_toolkit/__init__.py +2 -0
- whatsapp_toolkit-1.0.6/src/whatsapp_toolkit/whatsapp.py +383 -0
- whatsapp_toolkit-1.0.6/src/whatsapp_toolkit/whatsapp_instanced.py +25 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: whatsapp-toolkit
|
|
3
|
+
Version: 1.0.6
|
|
4
|
+
Summary: Una biblioteca para interactuar con la API de WhatsApp desde Python usando Evolution API
|
|
5
|
+
Author: Fernando Leon Franco
|
|
6
|
+
Author-email: Fernando Leon Franco <fernanlee2131@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Dist: colorstreak>=2.1.0
|
|
16
|
+
Requires-Dist: dotenv>=0.9.9
|
|
17
|
+
Requires-Dist: requests>=2.32.5
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Project-URL: Homepage, https://github.com/epok200/whatsapp_toolkit
|
|
20
|
+
Project-URL: Issues, https://github.com/epok200/whatsapp_toolkit/issues
|
|
21
|
+
Project-URL: Repository, https://github.com/epok200/whatsapp_toolkit
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Whatsapp Toolkit
|
|
26
|
+
|
|
27
|
+
Este módulo permite el envío de mensajes, archivos y multimedia por WhatsApp, con manejo avanzado de conexión, errores y administración de instancias.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Componentes principales
|
|
31
|
+
|
|
32
|
+
- **whatsapp.py**: Cliente principal para WhatsApp, incluye administración de instancias, conexión QR, envío de mensajes, archivos, multimedia, ubicación, audio y stickers.
|
|
33
|
+
- **whatsapp_instanced.py**: Utilidades avanzadas para instancias y flujos personalizados, incluyendo envío asíncrono de mensajes y archivos usando Celery.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Ejemplos y recomendaciones de uso
|
|
38
|
+
|
|
39
|
+
### 1. Inicialización y conexión
|
|
40
|
+
Conecta tu cliente y asegura la conexión antes de enviar mensajes.
|
|
41
|
+
```python
|
|
42
|
+
from epok_toolkit.messaging.whatsapp import WhatsappClient
|
|
43
|
+
|
|
44
|
+
client = WhatsappClient(api_key="tu_api_key", server_url="https://api.whatsapp.com", instance_name="EPOK")
|
|
45
|
+
client.ensure_connected() # Muestra QR y enlaza la instancia si es necesario
|
|
46
|
+
```
|
|
47
|
+
**Tip:** El método `ensure_connected` reintenta y muestra QR hasta enlazar la instancia.
|
|
48
|
+
|
|
49
|
+
### 2. Enviar mensajes de texto
|
|
50
|
+
```python
|
|
51
|
+
client.send_text(number="521234567890", text="Hola desde EPOK Toolkit!")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Enviar archivos y multimedia
|
|
55
|
+
Envía documentos, imágenes, stickers, audio y ubicación:
|
|
56
|
+
```python
|
|
57
|
+
# Enviar PDF
|
|
58
|
+
with open("ticket.pdf", "rb") as f:
|
|
59
|
+
import base64
|
|
60
|
+
pdf_b64 = base64.b64encode(f.read()).decode()
|
|
61
|
+
client.send_media(number="521234567890", media_b64=pdf_b64, filename="ticket.pdf", caption="Tu ticket", mediatype="document", mimetype="application/pdf")
|
|
62
|
+
|
|
63
|
+
# Enviar sticker
|
|
64
|
+
client.send_sticker(number="521234567890", sticker_b64=sticker_b64)
|
|
65
|
+
|
|
66
|
+
# Enviar ubicación
|
|
67
|
+
client.send_location(number="521234567890", name="EPOK", address="Calle 123", latitude=21.123, longitude=-101.456)
|
|
68
|
+
|
|
69
|
+
# Enviar audio
|
|
70
|
+
client.send_audio(number="521234567890", audio_b64=audio_b64)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 4. Administración de instancias y grupos
|
|
74
|
+
```python
|
|
75
|
+
client.create_instance() # Crea una nueva instancia en el servidor
|
|
76
|
+
client.delete_instance() # Elimina la instancia
|
|
77
|
+
client.fetch_groups() # Obtiene todos los grupos y participantes
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
### 6. Envío asíncrono con whatsapp_instanced.py
|
|
82
|
+
Envía mensajes y archivos en segundo plano usando Celery:
|
|
83
|
+
```python
|
|
84
|
+
from epok_toolkit.messaging.whatsapp_instanced import send_text, send_media
|
|
85
|
+
|
|
86
|
+
# Enviar mensaje de texto de forma asíncrona
|
|
87
|
+
send_text(number="521234567890", message="Hola desde EPOK Toolkit!")
|
|
88
|
+
|
|
89
|
+
# Enviar archivo PDF de forma asíncrona
|
|
90
|
+
send_media(number="521234567890", media_b64=pdf_b64, filename="ticket.pdf", caption="Tu ticket")
|
|
91
|
+
```
|
|
92
|
+
**Tip:** Configura Celery y los settings de API_KEY, INSTANCE y SERVER_URL para habilitar el envío asíncrono.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Más información
|
|
97
|
+
Consulta la documentación de cada archivo para detalles avanzados, recomendaciones y ejemplos específicos.
|
|
98
|
+
|
|
99
|
+
## Instalación
|
|
100
|
+
|
|
101
|
+
Con UV Package Manager:
|
|
102
|
+
```bash
|
|
103
|
+
uv add whatsapp-toolkit
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Con pip:
|
|
107
|
+
```bash
|
|
108
|
+
pip install whatsapp-toolkit
|
|
109
|
+
```
|
|
110
|
+
## Requisitos
|
|
111
|
+
- Python 3.10 o superior
|
|
112
|
+
- requests >=2.32.5
|
|
113
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
|
|
2
|
+
# Whatsapp Toolkit
|
|
3
|
+
|
|
4
|
+
Este módulo permite el envío de mensajes, archivos y multimedia por WhatsApp, con manejo avanzado de conexión, errores y administración de instancias.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Componentes principales
|
|
8
|
+
|
|
9
|
+
- **whatsapp.py**: Cliente principal para WhatsApp, incluye administración de instancias, conexión QR, envío de mensajes, archivos, multimedia, ubicación, audio y stickers.
|
|
10
|
+
- **whatsapp_instanced.py**: Utilidades avanzadas para instancias y flujos personalizados, incluyendo envío asíncrono de mensajes y archivos usando Celery.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Ejemplos y recomendaciones de uso
|
|
15
|
+
|
|
16
|
+
### 1. Inicialización y conexión
|
|
17
|
+
Conecta tu cliente y asegura la conexión antes de enviar mensajes.
|
|
18
|
+
```python
|
|
19
|
+
from epok_toolkit.messaging.whatsapp import WhatsappClient
|
|
20
|
+
|
|
21
|
+
client = WhatsappClient(api_key="tu_api_key", server_url="https://api.whatsapp.com", instance_name="EPOK")
|
|
22
|
+
client.ensure_connected() # Muestra QR y enlaza la instancia si es necesario
|
|
23
|
+
```
|
|
24
|
+
**Tip:** El método `ensure_connected` reintenta y muestra QR hasta enlazar la instancia.
|
|
25
|
+
|
|
26
|
+
### 2. Enviar mensajes de texto
|
|
27
|
+
```python
|
|
28
|
+
client.send_text(number="521234567890", text="Hola desde EPOK Toolkit!")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 3. Enviar archivos y multimedia
|
|
32
|
+
Envía documentos, imágenes, stickers, audio y ubicación:
|
|
33
|
+
```python
|
|
34
|
+
# Enviar PDF
|
|
35
|
+
with open("ticket.pdf", "rb") as f:
|
|
36
|
+
import base64
|
|
37
|
+
pdf_b64 = base64.b64encode(f.read()).decode()
|
|
38
|
+
client.send_media(number="521234567890", media_b64=pdf_b64, filename="ticket.pdf", caption="Tu ticket", mediatype="document", mimetype="application/pdf")
|
|
39
|
+
|
|
40
|
+
# Enviar sticker
|
|
41
|
+
client.send_sticker(number="521234567890", sticker_b64=sticker_b64)
|
|
42
|
+
|
|
43
|
+
# Enviar ubicación
|
|
44
|
+
client.send_location(number="521234567890", name="EPOK", address="Calle 123", latitude=21.123, longitude=-101.456)
|
|
45
|
+
|
|
46
|
+
# Enviar audio
|
|
47
|
+
client.send_audio(number="521234567890", audio_b64=audio_b64)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 4. Administración de instancias y grupos
|
|
51
|
+
```python
|
|
52
|
+
client.create_instance() # Crea una nueva instancia en el servidor
|
|
53
|
+
client.delete_instance() # Elimina la instancia
|
|
54
|
+
client.fetch_groups() # Obtiene todos los grupos y participantes
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
### 6. Envío asíncrono con whatsapp_instanced.py
|
|
59
|
+
Envía mensajes y archivos en segundo plano usando Celery:
|
|
60
|
+
```python
|
|
61
|
+
from epok_toolkit.messaging.whatsapp_instanced import send_text, send_media
|
|
62
|
+
|
|
63
|
+
# Enviar mensaje de texto de forma asíncrona
|
|
64
|
+
send_text(number="521234567890", message="Hola desde EPOK Toolkit!")
|
|
65
|
+
|
|
66
|
+
# Enviar archivo PDF de forma asíncrona
|
|
67
|
+
send_media(number="521234567890", media_b64=pdf_b64, filename="ticket.pdf", caption="Tu ticket")
|
|
68
|
+
```
|
|
69
|
+
**Tip:** Configura Celery y los settings de API_KEY, INSTANCE y SERVER_URL para habilitar el envío asíncrono.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Más información
|
|
74
|
+
Consulta la documentación de cada archivo para detalles avanzados, recomendaciones y ejemplos específicos.
|
|
75
|
+
|
|
76
|
+
## Instalación
|
|
77
|
+
|
|
78
|
+
Con UV Package Manager:
|
|
79
|
+
```bash
|
|
80
|
+
uv add whatsapp-toolkit
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Con pip:
|
|
84
|
+
```bash
|
|
85
|
+
pip install whatsapp-toolkit
|
|
86
|
+
```
|
|
87
|
+
## Requisitos
|
|
88
|
+
- Python 3.10 o superior
|
|
89
|
+
- requests >=2.32.5
|
|
90
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "whatsapp-toolkit"
|
|
3
|
+
version = "1.0.6"
|
|
4
|
+
description = "Una biblioteca para interactuar con la API de WhatsApp desde Python usando Evolution API"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name="Fernando Leon Franco", email="fernanlee2131@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
dependencies = [
|
|
12
|
+
"colorstreak>=2.1.0",
|
|
13
|
+
"dotenv>=0.9.9",
|
|
14
|
+
"requests>=2.32.5",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/epok200/whatsapp_toolkit"
|
|
29
|
+
Repository = "https://github.com/epok200/whatsapp_toolkit"
|
|
30
|
+
Issues = "https://github.com/epok200/whatsapp_toolkit/issues"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.9.17,<0.10.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def timeout_response(func):
|
|
8
|
+
@wraps(func)
|
|
9
|
+
def wrapper(*args, **kwargs):
|
|
10
|
+
try:
|
|
11
|
+
return func(*args, **kwargs)
|
|
12
|
+
except requests.Timeout:
|
|
13
|
+
print("La solicitud ha excedido el tiempo de espera.")
|
|
14
|
+
return HttpResponse(status_code=408, text="Timeout", json_data=None)
|
|
15
|
+
except requests.RequestException as e:
|
|
16
|
+
print(f"Error en la solicitud: {e}")
|
|
17
|
+
return HttpResponse(
|
|
18
|
+
status_code=500, text="Error", json_data={"error": str(e)}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return wrapper
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def require_connection(method):
|
|
25
|
+
"""
|
|
26
|
+
Decorador para métodos de WhatsappClient que necesitan una conexión activa.
|
|
27
|
+
Llama a `self.ensure_connected()` y solo ejecuta el método original si la
|
|
28
|
+
conexión se confirma; de lo contrario devuelve False.
|
|
29
|
+
"""
|
|
30
|
+
from functools import wraps
|
|
31
|
+
|
|
32
|
+
@wraps(method)
|
|
33
|
+
def _wrapper(self, *args, **kwargs):
|
|
34
|
+
if not self.ensure_connected():
|
|
35
|
+
print("❌ No fue posible establecer conexión.")
|
|
36
|
+
return False
|
|
37
|
+
return method(self, *args, **kwargs)
|
|
38
|
+
|
|
39
|
+
return _wrapper
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class HttpResponse:
|
|
44
|
+
status_code: int
|
|
45
|
+
text: str
|
|
46
|
+
json_data: Optional[dict] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WhatsAppInstance:
|
|
50
|
+
def __init__(self, api_key: str, instance: str, server_url: str):
|
|
51
|
+
self.api_key = api_key
|
|
52
|
+
self.name_instance = instance
|
|
53
|
+
self.status = "disconnected"
|
|
54
|
+
self.server_url = server_url.rstrip("/")
|
|
55
|
+
self.headers = {"apikey": self.api_key, "Content-Type": "application/json"}
|
|
56
|
+
|
|
57
|
+
def create_instance(self) -> HttpResponse:
|
|
58
|
+
"""Crea una nueva instancia de WhatsApp usando la API de Envole."""
|
|
59
|
+
url = f"{self.server_url}/instance/create"
|
|
60
|
+
payload = {
|
|
61
|
+
"instanceName": self.name_instance,
|
|
62
|
+
"integration": "WHATSAPP-BAILEYS",
|
|
63
|
+
"syncFullHistory": False,
|
|
64
|
+
}
|
|
65
|
+
response = requests.post(url, json=payload, headers=self.headers)
|
|
66
|
+
return HttpResponse(response.status_code, response.text, response.json())
|
|
67
|
+
|
|
68
|
+
def delete_instance(self) -> HttpResponse:
|
|
69
|
+
"""Elimina una instancia de WhatsApp usando la API de Envole."""
|
|
70
|
+
url = f"{self.server_url}/instance/delete/{self.name_instance}"
|
|
71
|
+
response = requests.delete(url, headers=self.headers)
|
|
72
|
+
return HttpResponse(response.status_code, response.text)
|
|
73
|
+
|
|
74
|
+
def show_qr(self, qr_text: str) -> None:
|
|
75
|
+
"""Genera un código QR a partir de `qr_text` y lo muestra con el visor por defecto."""
|
|
76
|
+
import qrcode
|
|
77
|
+
|
|
78
|
+
qr = qrcode.QRCode(border=2)
|
|
79
|
+
qr.add_data(qr_text)
|
|
80
|
+
qr.make(fit=True)
|
|
81
|
+
img = qr.make_image()
|
|
82
|
+
img.show()
|
|
83
|
+
|
|
84
|
+
def connect_instance_qr(self) -> None:
|
|
85
|
+
"""Conecta una instancia de WhatsApp y muestra una imagen"""
|
|
86
|
+
url = f"{self.server_url}/instance/connect/{self.name_instance}"
|
|
87
|
+
response = requests.get(url, headers=self.headers)
|
|
88
|
+
codigo = response.json().get("code")
|
|
89
|
+
self.show_qr(codigo)
|
|
90
|
+
|
|
91
|
+
def mode_connecting(self):
|
|
92
|
+
"""
|
|
93
|
+
Se intentará por 30 min el mantenter intentos de conexión a la instancia
|
|
94
|
+
generando un qr cada 10 segundos, si es exitoso se podra enviar un mensaje,
|
|
95
|
+
si después de eso no se conecta, se devolvera un error
|
|
96
|
+
"""
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class WhatsAppSender:
|
|
101
|
+
def __init__(self, instance: WhatsAppInstance):
|
|
102
|
+
self.instance = instance.name_instance
|
|
103
|
+
self.server_url = instance.server_url
|
|
104
|
+
self.headers = instance.headers
|
|
105
|
+
self._instance_obj = instance
|
|
106
|
+
self.connected = True # estado de conexión conocido
|
|
107
|
+
|
|
108
|
+
def test_connection_status(self) -> bool:
|
|
109
|
+
cel_epok = "5214778966517"
|
|
110
|
+
print(f"Probando conexión enviando mensaje a {cel_epok}...")
|
|
111
|
+
ok = bool(self.send_text(cel_epok, "ping"))
|
|
112
|
+
self.connected = ok
|
|
113
|
+
return ok
|
|
114
|
+
|
|
115
|
+
@timeout_response
|
|
116
|
+
def get(self, endpoint: str, params: Optional[dict] = None) -> requests.Response:
|
|
117
|
+
url = f"{self.server_url}{endpoint}"
|
|
118
|
+
return requests.get(url, headers=self.headers, params=params)
|
|
119
|
+
|
|
120
|
+
def put(self, endpoint: str) -> requests.Response:
|
|
121
|
+
url = f"{self.server_url}{endpoint}"
|
|
122
|
+
return requests.put(url, headers=self.headers)
|
|
123
|
+
|
|
124
|
+
def post(self, endpoint: str, payload: dict):
|
|
125
|
+
url = f"{self.server_url}{endpoint}"
|
|
126
|
+
request = requests.post(url, json=payload, headers=self.headers, timeout=10)
|
|
127
|
+
# if timeout:
|
|
128
|
+
try:
|
|
129
|
+
return request
|
|
130
|
+
except requests.Timeout:
|
|
131
|
+
print("Request timed out")
|
|
132
|
+
return HttpResponse(status_code=408, text="Timeout", json_data=None)
|
|
133
|
+
|
|
134
|
+
def send_text(self, number: str, text: str, link_preview: bool = True, delay_ms: int = 0) -> str:
|
|
135
|
+
payload = {
|
|
136
|
+
"number": number,
|
|
137
|
+
"text": text,
|
|
138
|
+
"delay": delay_ms,
|
|
139
|
+
"linkPreview": link_preview,
|
|
140
|
+
}
|
|
141
|
+
print(f"Enviando mensaje a {number}: {text}")
|
|
142
|
+
resp = self.post(f"/message/sendText/{self.instance}", payload)
|
|
143
|
+
|
|
144
|
+
# Si la solicitud se convirtió en HttpResponse por timeout
|
|
145
|
+
status = resp.status_code if hasattr(resp, "status_code") else 0
|
|
146
|
+
|
|
147
|
+
if 200 <= status < 300:
|
|
148
|
+
self.connected = True
|
|
149
|
+
return resp.text
|
|
150
|
+
|
|
151
|
+
# Fallo: marcar desconexión y reportar
|
|
152
|
+
print(f"Error al enviar mensaje a {number}: {status} - {resp.text}")
|
|
153
|
+
self.connected = False
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
def send_media(self, number: str, media_b64: str, filename: str, caption: str, mediatype: str = "document", mimetype: str = "application/pdf") -> str:
|
|
157
|
+
payload = {
|
|
158
|
+
"number": number,
|
|
159
|
+
"mediatype": mediatype,
|
|
160
|
+
"mimetype": mimetype,
|
|
161
|
+
"caption": caption,
|
|
162
|
+
"media": media_b64,
|
|
163
|
+
"fileName": filename,
|
|
164
|
+
"delay": 0,
|
|
165
|
+
"linkPreview": False,
|
|
166
|
+
"mentionsEveryOne": False,
|
|
167
|
+
}
|
|
168
|
+
resp = self.post(f"/message/sendMedia/{self.instance}", payload)
|
|
169
|
+
return resp.text
|
|
170
|
+
|
|
171
|
+
def send_sticker(self, number: str, sticker_b64: str, delay: int = 0, link_preview: bool = True, mentions_everyone: bool = True) -> str:
|
|
172
|
+
"""Envía un sticker a un contacto específico."""
|
|
173
|
+
payload = {
|
|
174
|
+
"number": number,
|
|
175
|
+
"sticker": sticker_b64,
|
|
176
|
+
"delay": delay,
|
|
177
|
+
"linkPreview": link_preview,
|
|
178
|
+
"mentionsEveryOne": mentions_everyone,
|
|
179
|
+
}
|
|
180
|
+
resp = self.post(f"/message/sendSticker/{self.instance}", payload)
|
|
181
|
+
return resp.text
|
|
182
|
+
|
|
183
|
+
def send_location(self, number: str, name: str, address: str, latitude: float, longitude: float, delay: int = 0) -> str:
|
|
184
|
+
"""Envía una ubicación a un contacto."""
|
|
185
|
+
payload = {
|
|
186
|
+
"number": number,
|
|
187
|
+
"name": name,
|
|
188
|
+
"address": address,
|
|
189
|
+
"latitude": latitude,
|
|
190
|
+
"longitude": longitude,
|
|
191
|
+
"delay": delay,
|
|
192
|
+
}
|
|
193
|
+
resp = self.post(f"/message/sendLocation/{self.instance}", payload)
|
|
194
|
+
return resp.text
|
|
195
|
+
|
|
196
|
+
def send_audio(self, number: str, audio_b64: str, delay: int = 0) -> str:
|
|
197
|
+
"""Envía un audio en formato base64 a un contacto."""
|
|
198
|
+
payload = {
|
|
199
|
+
"audio": audio_b64,
|
|
200
|
+
"number": number,
|
|
201
|
+
"delay": delay,
|
|
202
|
+
}
|
|
203
|
+
resp = self.post(f"/message/sendWhatsAppAudio/{self.instance}", payload)
|
|
204
|
+
return resp.text
|
|
205
|
+
|
|
206
|
+
def connect(self, number: str) -> str:
|
|
207
|
+
querystring = {"number": number}
|
|
208
|
+
resp = self.get(f"/instance/connect/{self.instance}", params=querystring)
|
|
209
|
+
return resp.text
|
|
210
|
+
|
|
211
|
+
def set_webhook(self, webhook_url: str, enabled: bool = True, webhook_by_events: bool = True, webhook_base64: bool = True, events: Optional[list] = None) -> str:
|
|
212
|
+
"""Configura el webhook para la instancia."""
|
|
213
|
+
if events is None:
|
|
214
|
+
events = ["SEND_MESSAGE"]
|
|
215
|
+
payload = {
|
|
216
|
+
"url": webhook_url,
|
|
217
|
+
"enabled": enabled,
|
|
218
|
+
"webhookByEvents": webhook_by_events,
|
|
219
|
+
"webhookBase64": webhook_base64,
|
|
220
|
+
"events": events,
|
|
221
|
+
}
|
|
222
|
+
resp = self.post(f"/webhook/set/{self.instance}", payload)
|
|
223
|
+
return resp.text
|
|
224
|
+
|
|
225
|
+
def fetch_groups(self, get_participants: bool = True) -> list:
|
|
226
|
+
"""Obtiene todos los grupos y sus participantes."""
|
|
227
|
+
params = {"getParticipants": str(get_participants).lower()}
|
|
228
|
+
resp = self.get(f"/group/fetchAllGroups/{self.instance}", params=params)
|
|
229
|
+
if resp.status_code == 200:
|
|
230
|
+
return resp.json()
|
|
231
|
+
else:
|
|
232
|
+
raise Exception(
|
|
233
|
+
f"Error al obtener grupos: {resp.status_code} - {resp.text}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def fetch_instances(api_key: str, server_url: str) -> list:
|
|
238
|
+
"""Obtiene todas las instancias disponibles en el servidor."""
|
|
239
|
+
url = f"{server_url}/instance/fetchInstances"
|
|
240
|
+
headers = {"apikey": api_key}
|
|
241
|
+
response = requests.get(url, headers=headers, verify=False)
|
|
242
|
+
# Puede ser una lista o dict, depende del backend
|
|
243
|
+
try:
|
|
244
|
+
return response.json()
|
|
245
|
+
except Exception:
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def get_instance_info(api_key: str, instance_name: str, server_url: str):
|
|
250
|
+
"""Busca la info de una instancia específica por nombre, robusto a diferentes formatos de respuesta."""
|
|
251
|
+
instances = WhatsAppSender.fetch_instances(api_key, server_url)
|
|
252
|
+
|
|
253
|
+
# Normalizar a lista para iterar
|
|
254
|
+
if isinstance(instances, dict):
|
|
255
|
+
instances = [instances]
|
|
256
|
+
# print(f"Buscando instancia: {instance_name} en {len(instances)} instancias disponibles.")
|
|
257
|
+
for item in instances:
|
|
258
|
+
data = (
|
|
259
|
+
item.get("instance")
|
|
260
|
+
if isinstance(item, dict) and "instance" in item
|
|
261
|
+
else item
|
|
262
|
+
)
|
|
263
|
+
# print(data)
|
|
264
|
+
if not isinstance(data, dict):
|
|
265
|
+
continue # Formato inesperado para us
|
|
266
|
+
|
|
267
|
+
if data.get("name") == instance_name:
|
|
268
|
+
return data
|
|
269
|
+
return {}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class WhatsappClient:
|
|
274
|
+
"""
|
|
275
|
+
Cliente para interactuar con la API de WhatsApp.
|
|
276
|
+
"""
|
|
277
|
+
def __init__(self, api_key: str, server_url: str, instance_name: str = "EPOK"):
|
|
278
|
+
self.instance = WhatsAppInstance(api_key, instance_name, server_url)
|
|
279
|
+
self.sender: Optional[WhatsAppSender] = None
|
|
280
|
+
self._auto_initialize_sender()
|
|
281
|
+
|
|
282
|
+
def _auto_initialize_sender(self):
|
|
283
|
+
"""Solo asigna sender si la instancia está enlazada a WhatsApp."""
|
|
284
|
+
info = WhatsAppSender.get_instance_info(
|
|
285
|
+
self.instance.api_key, self.instance.name_instance, self.instance.server_url
|
|
286
|
+
)
|
|
287
|
+
if info.get("ownerJid"): # <- si tiene owner, significa que ya está enlazada
|
|
288
|
+
self.sender = WhatsAppSender(self.instance)
|
|
289
|
+
|
|
290
|
+
def ensure_connected(self, retries: int = 3, delay: int = 30) -> bool:
|
|
291
|
+
"""
|
|
292
|
+
Garantiza que la instancia esté conectada.
|
|
293
|
+
Si aún no existe `self.sender`, intentará crearlo.
|
|
294
|
+
Si la prueba de conexión falla, muestra un QR y reintenta.
|
|
295
|
+
"""
|
|
296
|
+
import time
|
|
297
|
+
|
|
298
|
+
# Si ya tenemos sender y está marcado como conectado, salimos rápido
|
|
299
|
+
if self.sender and getattr(self.sender, "connected", False):
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
def _init_sender():
|
|
303
|
+
if self.sender is None:
|
|
304
|
+
# Intentar inicializar si la instancia ya está enlazada
|
|
305
|
+
info = WhatsAppSender.get_instance_info(
|
|
306
|
+
self.instance.api_key,
|
|
307
|
+
self.instance.name_instance,
|
|
308
|
+
self.instance.server_url,
|
|
309
|
+
)
|
|
310
|
+
if info.get("ownerJid"):
|
|
311
|
+
self.sender = WhatsAppSender(self.instance)
|
|
312
|
+
|
|
313
|
+
# Primer intento de inicializar el sender
|
|
314
|
+
_init_sender()
|
|
315
|
+
|
|
316
|
+
for attempt in range(1, retries + 1):
|
|
317
|
+
if self.sender and self.sender.test_connection_status():
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
print(
|
|
321
|
+
f"[{attempt}/{retries}] Conexión no disponible, mostrando nuevo QR (espera {delay}s)…"
|
|
322
|
+
)
|
|
323
|
+
self.instance.connect_instance_qr() # muestra nuevo QR
|
|
324
|
+
time.sleep(delay)
|
|
325
|
+
|
|
326
|
+
# Reintentar inicializar sender después de mostrar QR
|
|
327
|
+
_init_sender()
|
|
328
|
+
|
|
329
|
+
print("❌ No fue posible establecer conexión después de varios intentos.")
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
@require_connection
|
|
333
|
+
def send_text(self, number: str, text: str, link_preview: bool = True, delay_ms: int = 1000):
|
|
334
|
+
"""
|
|
335
|
+
Envía un mensaje de texto de WhatsApp a un número específico.
|
|
336
|
+
Args:
|
|
337
|
+
number (str): Número de teléfono de destino en formato internacional (por ejemplo, "+34123456789").
|
|
338
|
+
text (str): Contenido del mensaje de texto a enviar.
|
|
339
|
+
link_preview (bool, optional): Indica si se debe generar vista previa de enlaces incluidos en el mensaje.
|
|
340
|
+
Por defecto es True.
|
|
341
|
+
delay_ms (int, optional): Retraso en milisegundos antes de enviar el mensaje. Por defecto es 1000.
|
|
342
|
+
Returns:
|
|
343
|
+
Any: El resultado devuelto por `self.sender.send_text`, típicamente la respuesta del envío
|
|
344
|
+
proporcionada por la implementación concreta del `sender`.
|
|
345
|
+
Raises:
|
|
346
|
+
AttributeError: Si `self.sender` es None o no implementa el método `send_text`.
|
|
347
|
+
Exception: Cualquier error que pueda producirse durante el envío del mensaje.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
return self.sender.send_text(number, text, link_preview, delay_ms=delay_ms)
|
|
351
|
+
|
|
352
|
+
@require_connection
|
|
353
|
+
def send_media(self, number: str, media_b64: str, filename: str, caption: str, mediatype: str = "document", mimetype: str = "application/pdf"):
|
|
354
|
+
return self.sender.send_media(number, media_b64, filename, caption, mediatype, mimetype)
|
|
355
|
+
|
|
356
|
+
@require_connection
|
|
357
|
+
def send_sticker(self, number: str, sticker_b64: str, delay: int = 0, link_preview: bool = True, mentions_everyone: bool = True):
|
|
358
|
+
return self.sender.send_sticker(number, sticker_b64, delay, link_preview, mentions_everyone)
|
|
359
|
+
|
|
360
|
+
@require_connection
|
|
361
|
+
def send_location(self, number: str, name: str, address: str, latitude: float, longitude: float, delay: int = 0):
|
|
362
|
+
return self.sender.send_location(number, name, address, latitude, longitude, delay)
|
|
363
|
+
|
|
364
|
+
@require_connection
|
|
365
|
+
def send_audio(self, number: str, audio_b64: str, delay: int = 0):
|
|
366
|
+
return self.sender.send_audio(number, audio_b64, delay)
|
|
367
|
+
|
|
368
|
+
@require_connection
|
|
369
|
+
def connect_number(self, number: str):
|
|
370
|
+
return self.sender.connect(number)
|
|
371
|
+
|
|
372
|
+
@require_connection
|
|
373
|
+
def fetch_groups(self, get_participants: bool = True):
|
|
374
|
+
return self.sender.fetch_groups(get_participants)
|
|
375
|
+
|
|
376
|
+
def create_instance(self):
|
|
377
|
+
return self.instance.create_instance()
|
|
378
|
+
|
|
379
|
+
def delete_instance(self):
|
|
380
|
+
return self.instance.delete_instance()
|
|
381
|
+
|
|
382
|
+
def connect_instance_qr(self):
|
|
383
|
+
return self.instance.connect_instance_qr()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .whatsapp import WhatsappClient
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
load_dotenv()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
API_KEY = os.getenv("WHATSAPP_API_KEY")
|
|
9
|
+
# TODO: La instancia deberia ser creada y seteada (no desde variable de entorno)
|
|
10
|
+
INSTANCE = os.getenv("WHATSAPP_INSTANCE")
|
|
11
|
+
SERVER_URL = os.getenv("WHATSAPP_SERVER_URL")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def send_message(number: str, message: str, delay :int):
|
|
15
|
+
client = WhatsappClient(api_key=API_KEY, server_url=SERVER_URL, instance_name=INSTANCE)
|
|
16
|
+
return client.send_text(number, message, delay_ms=delay)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def send_media(number: str, media_b64: str, filename: str, caption: str, mediatype: str = "document", mimetype: str = "application/pdf"):
|
|
21
|
+
client = WhatsappClient(api_key=API_KEY, server_url=SERVER_URL, instance_name=INSTANCE)
|
|
22
|
+
return client.send_media(number, media_b64, filename, caption, mediatype, mimetype)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|