whatsapp-python 4.3.0__py3-none-any.whl → 4.4.0__py3-none-any.whl
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/__init__.py +213 -339
- whatsapp/async_ext/_buttons.py +110 -2
- whatsapp/async_ext/_media.py +4 -4
- whatsapp/async_ext/_message.py +15 -11
- whatsapp/async_ext/_send_media.py +39 -40
- whatsapp/async_ext/_send_others.py +2 -2
- whatsapp/async_ext/_static.py +16 -0
- whatsapp/constants.py +5 -1
- whatsapp/errors.py +61 -11
- whatsapp/ext/_buttons.py +96 -2
- whatsapp/ext/_media.py +4 -4
- whatsapp/ext/_message.py +16 -12
- whatsapp/ext/_send_media.py +39 -40
- whatsapp/ext/_send_others.py +2 -2
- whatsapp/ext/_static.py +16 -0
- whatsapp/graph_api.py +113 -0
- {whatsapp_python-4.3.0.dist-info → whatsapp_python-4.4.0.dist-info}/METADATA +17 -11
- whatsapp_python-4.4.0.dist-info/RECORD +22 -0
- {whatsapp_python-4.3.0.dist-info → whatsapp_python-4.4.0.dist-info}/WHEEL +1 -1
- {whatsapp_python-4.3.0.dist-info → whatsapp_python-4.4.0.dist-info}/licenses/LICENSE +1 -1
- whatsapp_python-4.3.0.dist-info/RECORD +0 -21
whatsapp/__init__.py
CHANGED
|
@@ -3,15 +3,19 @@ Unofficial Python wrapper for the WhatsApp Cloud API.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
import json
|
|
6
|
+
|
|
9
7
|
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Callable, Optional, Union
|
|
10
|
+
|
|
10
11
|
import aiohttp
|
|
11
|
-
|
|
12
|
+
import requests
|
|
12
13
|
from fastapi import FastAPI, HTTPException, Request
|
|
13
14
|
from uvicorn import run as _run
|
|
15
|
+
|
|
14
16
|
from .constants import VERSION
|
|
17
|
+
from .errors import Handle
|
|
18
|
+
from .graph_api import resolve_graph_api_version
|
|
15
19
|
from .ext._property import authorized
|
|
16
20
|
from .ext._send_others import send_custom_json, send_contacts
|
|
17
21
|
from .ext._message import send_template
|
|
@@ -24,7 +28,13 @@ from .ext._send_media import (
|
|
|
24
28
|
send_document,
|
|
25
29
|
)
|
|
26
30
|
from .ext._media import upload_media, query_media_url, download_media, delete_media
|
|
27
|
-
from .ext._buttons import
|
|
31
|
+
from .ext._buttons import (
|
|
32
|
+
send_button,
|
|
33
|
+
create_button,
|
|
34
|
+
send_reply_button,
|
|
35
|
+
send_cta_url,
|
|
36
|
+
send_location_request,
|
|
37
|
+
)
|
|
28
38
|
from .ext._static import (
|
|
29
39
|
is_message,
|
|
30
40
|
get_mobile,
|
|
@@ -42,6 +52,7 @@ from .ext._static import (
|
|
|
42
52
|
get_interactive_response,
|
|
43
53
|
get_location,
|
|
44
54
|
get_video,
|
|
55
|
+
get_reaction,
|
|
45
56
|
changed_field,
|
|
46
57
|
)
|
|
47
58
|
|
|
@@ -69,6 +80,8 @@ from .async_ext._buttons import (
|
|
|
69
80
|
send_button as async_send_button,
|
|
70
81
|
create_button as async_create_button,
|
|
71
82
|
send_reply_button as async_send_reply_button,
|
|
83
|
+
send_cta_url as async_send_cta_url,
|
|
84
|
+
send_location_request as async_send_location_request,
|
|
72
85
|
)
|
|
73
86
|
from .async_ext._static import (
|
|
74
87
|
is_message as async_is_message,
|
|
@@ -87,17 +100,60 @@ from .async_ext._static import (
|
|
|
87
100
|
get_interactive_response as async_get_interactive_response,
|
|
88
101
|
get_location as async_get_location,
|
|
89
102
|
get_video as async_get_video,
|
|
103
|
+
get_reaction as async_get_reaction,
|
|
90
104
|
changed_field as async_changed_field,
|
|
91
105
|
)
|
|
92
106
|
|
|
93
|
-
|
|
107
|
+
|
|
108
|
+
def _check_package_update(current_version: str) -> None:
|
|
109
|
+
try:
|
|
110
|
+
latest = str(
|
|
111
|
+
requests.get(
|
|
112
|
+
"https://pypi.org/pypi/whatsapp-python/json", timeout=10
|
|
113
|
+
).json()["info"]["version"]
|
|
114
|
+
)
|
|
115
|
+
except Exception as exc: # noqa: BLE001
|
|
116
|
+
logging.debug("Skipped package update check: %s", exc)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if current_version == latest:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
version_int = int(current_version.replace(".", ""))
|
|
124
|
+
except ValueError:
|
|
125
|
+
version_int = 0
|
|
126
|
+
try:
|
|
127
|
+
latest_int = int(latest.replace(".", ""))
|
|
128
|
+
except ValueError:
|
|
129
|
+
latest_int = 0
|
|
130
|
+
|
|
131
|
+
# Avoid false positives such as 1.0.10 vs 1.0.2 when using local builds.
|
|
132
|
+
if version_int >= latest_int:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if version_int == 0:
|
|
136
|
+
logging.critical(
|
|
137
|
+
"There was an error while checking for updates, please check for "
|
|
138
|
+
"updates manually. This may be due to the version being a "
|
|
139
|
+
"post-release version (e.g. 1.0.0.post1) or a pre-release version "
|
|
140
|
+
"(e.g. 1.0.0a1). READ THE CHANGELOG BEFORE UPDATING. NEW VERSIONS "
|
|
141
|
+
"MAY BREAK YOUR CODE IF NOT PROPERLY UPDATED."
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
logging.critical(
|
|
145
|
+
"Whatsapp-python is out of date. Please update to the latest "
|
|
146
|
+
"version %s. READ THE CHANGELOG BEFORE UPDATING. NEW VERSIONS MAY "
|
|
147
|
+
"BREAK YOUR CODE IF NOT PROPERLY UPDATED.",
|
|
148
|
+
latest,
|
|
149
|
+
)
|
|
94
150
|
|
|
95
151
|
|
|
96
152
|
class WhatsApp:
|
|
97
153
|
def __init__(
|
|
98
154
|
self,
|
|
99
155
|
token: str = "",
|
|
100
|
-
phone_number_id: dict = "",
|
|
156
|
+
phone_number_id: Union[dict, str] = "",
|
|
101
157
|
logger: bool = True,
|
|
102
158
|
update_check: bool = True,
|
|
103
159
|
verify_token: str = "",
|
|
@@ -109,11 +165,14 @@ class WhatsApp:
|
|
|
109
165
|
|
|
110
166
|
Args:
|
|
111
167
|
token[str]: Token for the WhatsApp cloud API obtained from the Facebook developer portal
|
|
112
|
-
phone_number_id[
|
|
168
|
+
phone_number_id[dict]: Mapping of friendly keys to phone number IDs from the developer portal
|
|
113
169
|
logger[bool]: Whether to enable logging or not (default: True)
|
|
170
|
+
update_check[bool]: Whether to check PyPI for package updates
|
|
171
|
+
verify_token[str]: Verification token used by the webhook challenge
|
|
172
|
+
debug[bool]: Whether to keep debug logs enabled
|
|
173
|
+
version[str]: Graph API version (``\"latest\"`` or e.g. ``\"v27.0\"``)
|
|
114
174
|
"""
|
|
115
175
|
|
|
116
|
-
# Check if the version is up to date
|
|
117
176
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
118
177
|
|
|
119
178
|
self.l = phone_number_id
|
|
@@ -121,7 +180,6 @@ class WhatsApp:
|
|
|
121
180
|
if isinstance(phone_number_id, dict):
|
|
122
181
|
# use first phone number id as default
|
|
123
182
|
phone_number_id = phone_number_id[list(phone_number_id.keys())[0]]
|
|
124
|
-
|
|
125
183
|
elif phone_number_id == "":
|
|
126
184
|
logging.error("Phone number ID not provided")
|
|
127
185
|
raise ValueError("Phone number ID not provided but required")
|
|
@@ -132,60 +190,12 @@ class WhatsApp:
|
|
|
132
190
|
raise ValueError(
|
|
133
191
|
"Phone number ID should be a dictionary of phone numbers and their IDs"
|
|
134
192
|
)
|
|
135
|
-
else:
|
|
136
|
-
pass
|
|
137
193
|
|
|
138
|
-
self.VERSION = VERSION
|
|
139
|
-
|
|
140
|
-
"https://developers.facebook.com/docs/graph-api/changelog/"
|
|
141
|
-
).text
|
|
142
|
-
|
|
143
|
-
# dynamically get the latest version of the API
|
|
144
|
-
if version == "latest":
|
|
145
|
-
soup = BeautifulSoup(r, features="html.parser")
|
|
146
|
-
t1 = soup.findAll("table")
|
|
147
|
-
|
|
148
|
-
def makeversion(table: BeautifulSoup) -> str:
|
|
149
|
-
result = []
|
|
150
|
-
allrows = table.findAll("tr")
|
|
151
|
-
for row in allrows:
|
|
152
|
-
result.append([])
|
|
153
|
-
allcols = row.findAll("td")
|
|
154
|
-
for col in allcols:
|
|
155
|
-
thestrings = [(s) for s in col.findAll(text=True)]
|
|
156
|
-
thetext = "".join(thestrings)
|
|
157
|
-
result[-1].append(thetext)
|
|
158
|
-
return result[0][1]
|
|
159
|
-
|
|
160
|
-
self.LATEST = makeversion(t1[0]) # latest version of the API
|
|
161
|
-
else:
|
|
162
|
-
self.LATEST = version
|
|
194
|
+
self.VERSION = VERSION
|
|
195
|
+
self.LATEST = resolve_graph_api_version(version)
|
|
163
196
|
|
|
164
197
|
if update_check is True:
|
|
165
|
-
|
|
166
|
-
requests.get("https://pypi.org/pypi/whatsapp-python/json").json()[
|
|
167
|
-
"info"
|
|
168
|
-
]["version"]
|
|
169
|
-
)
|
|
170
|
-
if self.VERSION != latest:
|
|
171
|
-
try:
|
|
172
|
-
version_int = int(self.VERSION.replace(".", ""))
|
|
173
|
-
except:
|
|
174
|
-
version_int = 0
|
|
175
|
-
try:
|
|
176
|
-
latest_int = int(latest.replace(".", ""))
|
|
177
|
-
except:
|
|
178
|
-
latest_int = 0
|
|
179
|
-
# this is to avoid the case where the version is 1.0.10 and the latest is 1.0.2 (possible if user is using the github version)
|
|
180
|
-
if version_int < latest_int:
|
|
181
|
-
if version_int == 0:
|
|
182
|
-
logging.critical(
|
|
183
|
-
f"There was an error while checking for updates, please check for updates manually. This may be due to the version being a post-release version (e.g. 1.0.0.post1) or a pre-release version (e.g. 1.0.0a1). READ THE CHANGELOG BEFORE UPDATING. NEW VERSIONS MAY BREAK YOUR CODE IF NOT PROPERLY UPDATED."
|
|
184
|
-
)
|
|
185
|
-
else:
|
|
186
|
-
logging.critical(
|
|
187
|
-
f"Whatsapp-python is out of date. Please update to the latest version {latest}. READ THE CHANGELOG BEFORE UPDATING. NEW VERSIONS MAY BREAK YOUR CODE IF NOT PROPERLY UPDATED."
|
|
188
|
-
)
|
|
198
|
+
_check_package_update(self.VERSION)
|
|
189
199
|
|
|
190
200
|
if token == "":
|
|
191
201
|
logging.error("Token not provided")
|
|
@@ -193,6 +203,7 @@ class WhatsApp:
|
|
|
193
203
|
if phone_number_id == "":
|
|
194
204
|
logging.error("Phone number ID not provided")
|
|
195
205
|
raise ValueError("Phone number ID not provided but required")
|
|
206
|
+
|
|
196
207
|
self.token = token
|
|
197
208
|
self.phone_number_id = phone_number_id
|
|
198
209
|
self.base_url = f"https://graph.facebook.com/{self.LATEST}"
|
|
@@ -240,15 +251,15 @@ class WhatsApp:
|
|
|
240
251
|
data = await r.json()
|
|
241
252
|
if data is None:
|
|
242
253
|
return {"success": False}
|
|
243
|
-
|
|
244
|
-
#
|
|
245
|
-
logging.debug(
|
|
254
|
+
# Avoid logging raw webhook payloads — they contain phone numbers
|
|
255
|
+
# and message content that CodeQL treats as sensitive clear-text data.
|
|
256
|
+
logging.debug("Received webhook payload")
|
|
246
257
|
|
|
247
|
-
|
|
248
|
-
if
|
|
258
|
+
changed = self.changed_field(data)
|
|
259
|
+
if changed == "messages":
|
|
249
260
|
new_message = self.is_message(data)
|
|
250
261
|
if new_message:
|
|
251
|
-
msg =
|
|
262
|
+
msg = self.create_message(data=data)
|
|
252
263
|
await self.message_handler(msg)
|
|
253
264
|
await self.other_handler(msg)
|
|
254
265
|
return {"success": True}
|
|
@@ -275,12 +286,15 @@ class WhatsApp:
|
|
|
275
286
|
get_interactive_response = staticmethod(get_interactive_response)
|
|
276
287
|
get_location = staticmethod(get_location)
|
|
277
288
|
get_video = staticmethod(get_video)
|
|
289
|
+
get_reaction = staticmethod(get_reaction)
|
|
278
290
|
changed_field = staticmethod(changed_field)
|
|
279
291
|
get_author = staticmethod(get_author)
|
|
280
292
|
|
|
281
293
|
send_button = send_button
|
|
282
294
|
create_button = create_button
|
|
283
295
|
send_reply_button = send_reply_button
|
|
296
|
+
send_cta_url = send_cta_url
|
|
297
|
+
send_location_request = send_location_request
|
|
284
298
|
send_image = send_image
|
|
285
299
|
send_video = send_video
|
|
286
300
|
send_audio = send_audio
|
|
@@ -296,7 +310,7 @@ class WhatsApp:
|
|
|
296
310
|
send_contacts = send_contacts
|
|
297
311
|
authorized = property(authorized)
|
|
298
312
|
|
|
299
|
-
def create_message(self, **kwargs) -> Message:
|
|
313
|
+
def create_message(self, **kwargs) -> "Message":
|
|
300
314
|
"""
|
|
301
315
|
Create a message object
|
|
302
316
|
|
|
@@ -308,7 +322,7 @@ class WhatsApp:
|
|
|
308
322
|
"""
|
|
309
323
|
return Message(**kwargs, instance=self)
|
|
310
324
|
|
|
311
|
-
def on_message(self, handler:
|
|
325
|
+
def on_message(self, handler: Callable[..., Any]):
|
|
312
326
|
"""
|
|
313
327
|
Set the handler for incoming messages
|
|
314
328
|
|
|
@@ -317,7 +331,7 @@ class WhatsApp:
|
|
|
317
331
|
"""
|
|
318
332
|
self.message_handler = handler
|
|
319
333
|
|
|
320
|
-
def on_event(self, handler:
|
|
334
|
+
def on_event(self, handler: Callable[..., Any]):
|
|
321
335
|
"""
|
|
322
336
|
Set the handler for other events
|
|
323
337
|
|
|
@@ -326,7 +340,7 @@ class WhatsApp:
|
|
|
326
340
|
"""
|
|
327
341
|
self.other_handler = handler
|
|
328
342
|
|
|
329
|
-
def on_verification(self, handler:
|
|
343
|
+
def on_verification(self, handler: Callable[..., Any]):
|
|
330
344
|
"""
|
|
331
345
|
Set the handler for verification
|
|
332
346
|
|
|
@@ -340,192 +354,32 @@ class WhatsApp:
|
|
|
340
354
|
|
|
341
355
|
|
|
342
356
|
class AsyncWhatsApp(WhatsApp):
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
364
|
-
self.l = phone_number_id
|
|
365
|
-
|
|
366
|
-
if isinstance(phone_number_id, dict):
|
|
367
|
-
# use first phone number id as default
|
|
368
|
-
phone_number_id = phone_number_id[list(phone_number_id.keys())[0]]
|
|
369
|
-
|
|
370
|
-
elif phone_number_id == "":
|
|
371
|
-
logging.error("Phone number ID not provided")
|
|
372
|
-
raise ValueError("Phone number ID not provided but required")
|
|
373
|
-
elif isinstance(phone_number_id, str):
|
|
374
|
-
logging.critical(
|
|
375
|
-
"The phone number ID should be a dictionary of phone numbers and their IDs. Using strings is deprecated."
|
|
376
|
-
)
|
|
377
|
-
raise ValueError(
|
|
378
|
-
"Phone number ID should be a dictionary of phone numbers and their IDs"
|
|
379
|
-
)
|
|
380
|
-
else:
|
|
381
|
-
pass
|
|
382
|
-
|
|
383
|
-
self.VERSION = VERSION # package version
|
|
384
|
-
r = requests.get(
|
|
385
|
-
"https://developers.facebook.com/docs/graph-api/changelog/"
|
|
386
|
-
).text
|
|
387
|
-
|
|
388
|
-
# dynamically get the latest version of the API
|
|
389
|
-
if version == "latest":
|
|
390
|
-
soup = BeautifulSoup(r, features="html.parser")
|
|
391
|
-
t1 = soup.findAll("table")
|
|
392
|
-
|
|
393
|
-
def makeversion(table: BeautifulSoup) -> str:
|
|
394
|
-
result = []
|
|
395
|
-
allrows = table.findAll("tr")
|
|
396
|
-
for row in allrows:
|
|
397
|
-
result.append([])
|
|
398
|
-
allcols = row.findAll("td")
|
|
399
|
-
for col in allcols:
|
|
400
|
-
thestrings = [(s) for s in col.findAll(text=True)]
|
|
401
|
-
thetext = "".join(thestrings)
|
|
402
|
-
result[-1].append(thetext)
|
|
403
|
-
return result[0][1]
|
|
404
|
-
|
|
405
|
-
self.LATEST = makeversion(t1[0]) # latest version of the API
|
|
406
|
-
else:
|
|
407
|
-
self.LATEST = version
|
|
408
|
-
|
|
409
|
-
if update_check is True:
|
|
410
|
-
latest = str(
|
|
411
|
-
requests.get("https://pypi.org/pypi/whatsapp-python/json").json()[
|
|
412
|
-
"info"
|
|
413
|
-
]["version"]
|
|
414
|
-
)
|
|
415
|
-
if self.VERSION != latest:
|
|
416
|
-
try:
|
|
417
|
-
version_int = int(self.VERSION.replace(".", ""))
|
|
418
|
-
except:
|
|
419
|
-
version_int = 0
|
|
420
|
-
try:
|
|
421
|
-
latest_int = int(latest.replace(".", ""))
|
|
422
|
-
except:
|
|
423
|
-
latest_int = 0
|
|
424
|
-
# this is to avoid the case where the version is 1.0.10 and the latest is 1.0.2 (possible if user is using the github version)
|
|
425
|
-
if version_int < latest_int:
|
|
426
|
-
if version_int == 0:
|
|
427
|
-
logging.critical(
|
|
428
|
-
f"There was an error while checking for updates, please check for updates manually. This may be due to the version being a post-release version (e.g. 1.0.0.post1) or a pre-release version (e.g. 1.0.0a1). READ THE CHANGELOG BEFORE UPDATING. NEW VERSIONS MAY BREAK YOUR CODE IF NOT PROPERLY UPDATED."
|
|
429
|
-
)
|
|
430
|
-
else:
|
|
431
|
-
logging.critical(
|
|
432
|
-
f"Whatsapp-python is out of date. Please update to the latest version {latest}. READ THE CHANGELOG BEFORE UPDATING. NEW VERSIONS MAY BREAK YOUR CODE IF NOT PROPERLY UPDATED."
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
if token == "":
|
|
436
|
-
logging.error("Token not provided")
|
|
437
|
-
raise ValueError("Token not provided but required")
|
|
438
|
-
if phone_number_id == "":
|
|
439
|
-
logging.error("Phone number ID not provided")
|
|
440
|
-
raise ValueError("Phone number ID not provided but required")
|
|
441
|
-
self.token = token
|
|
442
|
-
self.phone_number_id = phone_number_id
|
|
443
|
-
self.base_url = f"https://graph.facebook.com/{self.LATEST}"
|
|
444
|
-
self.url = f"{self.base_url}/{phone_number_id}/messages"
|
|
445
|
-
self.verify_token = verify_token
|
|
446
|
-
|
|
447
|
-
async def base(*args):
|
|
448
|
-
pass
|
|
449
|
-
|
|
450
|
-
self.message_handler = base
|
|
451
|
-
self.other_handler = base
|
|
452
|
-
self.verification_handler = base
|
|
453
|
-
self.headers = {
|
|
454
|
-
"Content-Type": "application/json",
|
|
455
|
-
"Authorization": f"Bearer {self.token}",
|
|
456
|
-
}
|
|
457
|
-
if logger is False:
|
|
458
|
-
logging.disable(logging.INFO)
|
|
459
|
-
logging.disable(logging.ERROR)
|
|
460
|
-
if debug is False:
|
|
461
|
-
logging.disable(logging.DEBUG)
|
|
462
|
-
logging.disable(logging.ERROR)
|
|
463
|
-
|
|
464
|
-
self.app = FastAPI()
|
|
465
|
-
|
|
466
|
-
# Verification handler has 1 argument: challenge (str | bool): str if verification is successful, False if not
|
|
467
|
-
|
|
468
|
-
@self.app.get("/")
|
|
469
|
-
async def verify_endpoint(r: Request):
|
|
470
|
-
if r.query_params.get("hub.verify_token") == self.verify_token:
|
|
471
|
-
logging.debug("Webhook verified successfully")
|
|
472
|
-
challenge = r.query_params.get("hub.challenge")
|
|
473
|
-
await self.verification_handler(challenge)
|
|
474
|
-
await self.other_handler(challenge)
|
|
475
|
-
return int(challenge)
|
|
476
|
-
logging.error("Webhook Verification failed - token mismatch")
|
|
477
|
-
await self.verification_handler(False)
|
|
478
|
-
await self.other_handler(False)
|
|
479
|
-
return {"success": False}
|
|
480
|
-
|
|
481
|
-
@self.app.post("/")
|
|
482
|
-
async def hook(r: Request):
|
|
483
|
-
try:
|
|
484
|
-
# Handle Webhook Subscriptions
|
|
485
|
-
data = await r.json()
|
|
486
|
-
if data is None:
|
|
487
|
-
return {"success": False}
|
|
488
|
-
data_str = json.dumps(data, indent=4)
|
|
489
|
-
# log the data received only if the log level is debug
|
|
490
|
-
logging.debug(f"Received webhook data: {data_str}")
|
|
491
|
-
|
|
492
|
-
changed_field = self.changed_field(data)
|
|
493
|
-
if changed_field == "messages":
|
|
494
|
-
new_message = self.is_message(data)
|
|
495
|
-
if new_message:
|
|
496
|
-
msg = Message(instance=self, data=data)
|
|
497
|
-
await self.message_handler(msg)
|
|
498
|
-
await self.other_handler(msg)
|
|
499
|
-
return {"success": True}
|
|
500
|
-
except Exception as e:
|
|
501
|
-
logging.error(f"Error parsing message: {e}")
|
|
502
|
-
raise HTTPException(
|
|
503
|
-
status_code=500, detail={"success": False, "error": str(e)}
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
# all the files starting with _ are imported here, and should not be imported directly.
|
|
507
|
-
|
|
508
|
-
is_message = staticmethod(is_message)
|
|
509
|
-
get_mobile = staticmethod(get_mobile)
|
|
510
|
-
get_name = staticmethod(get_name)
|
|
511
|
-
get_message = staticmethod(get_message)
|
|
512
|
-
get_message_id = staticmethod(get_message_id)
|
|
513
|
-
get_message_type = staticmethod(get_message_type)
|
|
514
|
-
get_message_timestamp = staticmethod(get_message_timestamp)
|
|
515
|
-
get_audio = staticmethod(get_audio)
|
|
516
|
-
get_delivery = staticmethod(get_delivery)
|
|
517
|
-
get_document = staticmethod(get_document)
|
|
518
|
-
get_image = staticmethod(get_image)
|
|
519
|
-
get_sticker = staticmethod(get_sticker)
|
|
520
|
-
get_interactive_response = staticmethod(get_interactive_response)
|
|
521
|
-
get_location = staticmethod(get_location)
|
|
522
|
-
get_video = staticmethod(get_video)
|
|
523
|
-
changed_field = staticmethod(changed_field)
|
|
524
|
-
get_author = staticmethod(get_author)
|
|
357
|
+
"""Async variant of :class:`WhatsApp` that returns awaitable HTTP calls."""
|
|
358
|
+
|
|
359
|
+
is_message = staticmethod(async_is_message)
|
|
360
|
+
get_mobile = staticmethod(async_get_mobile)
|
|
361
|
+
get_name = staticmethod(async_get_name)
|
|
362
|
+
get_message = staticmethod(async_get_message)
|
|
363
|
+
get_message_id = staticmethod(async_get_message_id)
|
|
364
|
+
get_message_type = staticmethod(async_get_message_type)
|
|
365
|
+
get_message_timestamp = staticmethod(async_get_message_timestamp)
|
|
366
|
+
get_audio = staticmethod(async_get_audio)
|
|
367
|
+
get_delivery = staticmethod(async_get_delivery)
|
|
368
|
+
get_document = staticmethod(async_get_document)
|
|
369
|
+
get_image = staticmethod(async_get_image)
|
|
370
|
+
get_sticker = staticmethod(async_get_sticker)
|
|
371
|
+
get_interactive_response = staticmethod(async_get_interactive_response)
|
|
372
|
+
get_location = staticmethod(async_get_location)
|
|
373
|
+
get_video = staticmethod(async_get_video)
|
|
374
|
+
get_reaction = staticmethod(async_get_reaction)
|
|
375
|
+
changed_field = staticmethod(async_changed_field)
|
|
376
|
+
get_author = staticmethod(async_get_author)
|
|
525
377
|
|
|
526
378
|
send_button = async_send_button
|
|
527
379
|
create_button = async_create_button
|
|
528
380
|
send_reply_button = async_send_reply_button
|
|
381
|
+
send_cta_url = async_send_cta_url
|
|
382
|
+
send_location_request = async_send_location_request
|
|
529
383
|
send_image = async_send_image
|
|
530
384
|
send_video = async_send_video
|
|
531
385
|
send_audio = async_send_audio
|
|
@@ -540,13 +394,13 @@ class AsyncWhatsApp(WhatsApp):
|
|
|
540
394
|
send_custom_json = async_send_custom_json
|
|
541
395
|
send_contacts = async_send_contacts
|
|
542
396
|
authorized = property(async_authorized)
|
|
543
|
-
|
|
397
|
+
|
|
544
398
|
def handle(self, data: dict):
|
|
545
399
|
return Handle(data)
|
|
546
400
|
|
|
547
|
-
def create_message(self, **kwargs) -> AsyncMessage:
|
|
401
|
+
def create_message(self, **kwargs) -> "AsyncMessage":
|
|
548
402
|
"""
|
|
549
|
-
Create
|
|
403
|
+
Create an async message object
|
|
550
404
|
|
|
551
405
|
Args:
|
|
552
406
|
data[dict]: The message data
|
|
@@ -556,107 +410,93 @@ class AsyncWhatsApp(WhatsApp):
|
|
|
556
410
|
"""
|
|
557
411
|
return AsyncMessage(**kwargs, instance=self)
|
|
558
412
|
|
|
559
|
-
def on_message(self, handler: function):
|
|
560
|
-
"""
|
|
561
|
-
Set the handler for incoming messages
|
|
562
|
-
|
|
563
|
-
Args:
|
|
564
|
-
handler[function]: The handler function
|
|
565
|
-
"""
|
|
566
|
-
self.message_handler = handler
|
|
567
|
-
|
|
568
|
-
def on_event(self, handler: function):
|
|
569
|
-
"""
|
|
570
|
-
Set the handler for other events
|
|
571
|
-
|
|
572
|
-
Args:
|
|
573
|
-
handler[function]: The handler function
|
|
574
|
-
"""
|
|
575
|
-
self.other_handler = handler
|
|
576
|
-
|
|
577
|
-
def on_verification(self, handler: function):
|
|
578
|
-
"""
|
|
579
|
-
Set the handler for verification
|
|
580
|
-
|
|
581
|
-
Args:
|
|
582
|
-
handler[function]: The handler function
|
|
583
|
-
"""
|
|
584
|
-
self.verification_handler = handler
|
|
585
|
-
|
|
586
|
-
def run(self, host: str = "localhost", port: int = 5000, **options):
|
|
587
|
-
_run(self.app, host=host, port=port, **options)
|
|
588
|
-
|
|
589
413
|
|
|
590
414
|
class Message:
|
|
591
415
|
# type: ignore
|
|
592
416
|
def __init__(
|
|
593
417
|
self,
|
|
594
|
-
id:
|
|
595
|
-
data: dict =
|
|
596
|
-
instance: WhatsApp = None,
|
|
418
|
+
id: Optional[str] = None,
|
|
419
|
+
data: Optional[dict] = None,
|
|
420
|
+
instance: Optional[WhatsApp] = None,
|
|
597
421
|
content: str = "",
|
|
598
422
|
to: str = "",
|
|
599
423
|
rec_type: str = "individual",
|
|
600
424
|
):
|
|
425
|
+
data = data or {}
|
|
601
426
|
self.instance = instance
|
|
602
427
|
self.url = self.instance.url
|
|
603
428
|
self.headers = self.instance.headers
|
|
604
429
|
|
|
605
430
|
try:
|
|
606
431
|
self.id = instance.get_message_id(data)
|
|
607
|
-
except:
|
|
432
|
+
except Exception:
|
|
608
433
|
self.id = id
|
|
609
434
|
try:
|
|
610
435
|
self.type = self.instance.get_message_type(data)
|
|
611
|
-
except:
|
|
436
|
+
except Exception:
|
|
612
437
|
self.type = "text"
|
|
613
438
|
self.data = data
|
|
614
439
|
self.rec = rec_type
|
|
615
|
-
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
self.to = to if to != "" else self.instance.get_mobile(data)
|
|
443
|
+
except Exception:
|
|
444
|
+
self.to = to
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
self.sender = self.instance.get_author(data) or self.to
|
|
448
|
+
except Exception:
|
|
449
|
+
self.sender = self.to
|
|
450
|
+
|
|
616
451
|
try:
|
|
617
452
|
self.content = content if content != "" else self.instance.get_message(data)
|
|
618
|
-
except:
|
|
453
|
+
except Exception:
|
|
619
454
|
self.content = content
|
|
620
455
|
try:
|
|
621
456
|
self.name = self.instance.get_name(data)
|
|
622
|
-
except:
|
|
457
|
+
except Exception:
|
|
623
458
|
self.name = None
|
|
624
459
|
|
|
625
460
|
if self.type == "image":
|
|
626
461
|
try:
|
|
627
462
|
self.image = self.instance.get_image(data)
|
|
628
|
-
except:
|
|
463
|
+
except Exception:
|
|
629
464
|
self.image = None
|
|
630
465
|
if self.type == "sticker":
|
|
631
466
|
try:
|
|
632
467
|
self.sticker = self.instance.get_sticker(data)
|
|
633
|
-
except:
|
|
468
|
+
except Exception:
|
|
634
469
|
self.sticker = None
|
|
635
470
|
elif self.type == "video":
|
|
636
471
|
try:
|
|
637
472
|
self.video = self.instance.get_video(data)
|
|
638
|
-
except:
|
|
473
|
+
except Exception:
|
|
639
474
|
self.video = None
|
|
640
475
|
elif self.type == "audio":
|
|
641
476
|
try:
|
|
642
477
|
self.audio = self.instance.get_audio(data)
|
|
643
|
-
except:
|
|
478
|
+
except Exception:
|
|
644
479
|
pass
|
|
645
480
|
elif self.type == "document":
|
|
646
481
|
try:
|
|
647
482
|
self.document = self.instance.get_document(data)
|
|
648
|
-
except:
|
|
483
|
+
except Exception:
|
|
649
484
|
pass
|
|
650
485
|
elif self.type == "location":
|
|
651
486
|
try:
|
|
652
487
|
self.location = self.instance.get_location(data)
|
|
653
|
-
except:
|
|
488
|
+
except Exception:
|
|
654
489
|
pass
|
|
655
490
|
elif self.type == "interactive":
|
|
656
491
|
try:
|
|
657
492
|
self.interactive = self.instance.get_interactive_response(data)
|
|
658
|
-
except:
|
|
493
|
+
except Exception:
|
|
659
494
|
pass
|
|
495
|
+
elif self.type == "reaction":
|
|
496
|
+
try:
|
|
497
|
+
self.reaction = self.instance.get_reaction(data)
|
|
498
|
+
except Exception:
|
|
499
|
+
self.reaction = None
|
|
660
500
|
|
|
661
501
|
def reply(self, reply_text: str = "", preview_url: bool = True) -> dict:
|
|
662
502
|
if self.data == {}:
|
|
@@ -677,36 +517,47 @@ class Message:
|
|
|
677
517
|
return r.json()
|
|
678
518
|
logging.info(f"Message not sent to {self.instance.get_author(self.data)}")
|
|
679
519
|
logging.info(f"Status code: {r.status_code}")
|
|
680
|
-
logging.error(
|
|
520
|
+
logging.error("Request failed (status=%s)", r.status_code)
|
|
681
521
|
return r.json()
|
|
682
522
|
|
|
683
|
-
def mark_as_read(self) -> dict:
|
|
523
|
+
def mark_as_read(self, typing: bool = False) -> dict:
|
|
524
|
+
"""
|
|
525
|
+
Mark this inbound message as read.
|
|
684
526
|
|
|
685
|
-
|
|
527
|
+
Args:
|
|
528
|
+
typing: When True, also display a typing indicator (dismissed after
|
|
529
|
+
a reply or ~25 seconds). See Meta typing-indicators docs.
|
|
530
|
+
"""
|
|
531
|
+
payload: dict[str, Any] = {
|
|
686
532
|
"messaging_product": "whatsapp",
|
|
687
533
|
"status": "read",
|
|
688
534
|
"message_id": self.id,
|
|
689
535
|
}
|
|
536
|
+
if typing:
|
|
537
|
+
payload["typing_indicator"] = {"type": "text"}
|
|
690
538
|
|
|
691
539
|
response = requests.post(
|
|
692
540
|
f"{self.instance.url}", headers=self.instance.headers, json=payload
|
|
693
541
|
)
|
|
694
542
|
if response.status_code == 200:
|
|
695
|
-
logging.info(response.
|
|
696
|
-
return response.json()
|
|
697
|
-
else:
|
|
698
|
-
logging.error(response.json())
|
|
543
|
+
logging.info("Request succeeded (status=%s)", response.status_code)
|
|
699
544
|
return response.json()
|
|
545
|
+
logging.error("Request failed (status=%s)", response.status_code)
|
|
546
|
+
return response.json()
|
|
547
|
+
|
|
548
|
+
def show_typing(self) -> dict:
|
|
549
|
+
"""Mark as read and show a typing indicator while preparing a reply."""
|
|
550
|
+
return self.mark_as_read(typing=True)
|
|
700
551
|
|
|
701
552
|
def send(self, sender=None, preview_url: bool = True) -> dict:
|
|
702
553
|
|
|
703
554
|
try:
|
|
704
555
|
sender = dict(self.instance.l)[sender]
|
|
705
556
|
|
|
706
|
-
except:
|
|
557
|
+
except Exception:
|
|
707
558
|
sender = self.instance.phone_number_id
|
|
708
559
|
|
|
709
|
-
if sender
|
|
560
|
+
if sender is None:
|
|
710
561
|
sender = self.instance.phone_number_id
|
|
711
562
|
|
|
712
563
|
url = f"https://graph.facebook.com/{self.instance.LATEST}/{sender}/messages"
|
|
@@ -724,7 +575,7 @@ class Message:
|
|
|
724
575
|
return r.json()
|
|
725
576
|
logging.info(f"Message not sent to {self.to}")
|
|
726
577
|
logging.info(f"Status code: {r.status_code}")
|
|
727
|
-
logging.error(
|
|
578
|
+
logging.error("Request failed (status=%s)", r.status_code)
|
|
728
579
|
return r.json()
|
|
729
580
|
|
|
730
581
|
def react(self, emoji: str) -> dict:
|
|
@@ -742,7 +593,7 @@ class Message:
|
|
|
742
593
|
return r.json()
|
|
743
594
|
logging.info(f"Reaction not sent to {self.to}")
|
|
744
595
|
logging.info(f"Status code: {r.status_code}")
|
|
745
|
-
logging.debug(
|
|
596
|
+
logging.debug("Request failed (status=%s)", r.status_code)
|
|
746
597
|
return r.json()
|
|
747
598
|
|
|
748
599
|
|
|
@@ -750,72 +601,85 @@ class AsyncMessage:
|
|
|
750
601
|
# type: ignore
|
|
751
602
|
def __init__(
|
|
752
603
|
self,
|
|
753
|
-
id:
|
|
754
|
-
data: dict =
|
|
755
|
-
instance: WhatsApp = None,
|
|
604
|
+
id: Optional[str] = None,
|
|
605
|
+
data: Optional[dict] = None,
|
|
606
|
+
instance: Optional[WhatsApp] = None,
|
|
756
607
|
content: str = "",
|
|
757
608
|
to: str = "",
|
|
758
609
|
rec_type: str = "individual",
|
|
759
610
|
):
|
|
611
|
+
data = data or {}
|
|
760
612
|
self.instance = instance
|
|
761
613
|
self.url = self.instance.url
|
|
762
614
|
self.headers = self.instance.headers
|
|
763
615
|
|
|
764
616
|
try:
|
|
765
617
|
self.id = instance.get_message_id(data)
|
|
766
|
-
except:
|
|
618
|
+
except Exception:
|
|
767
619
|
self.id = id
|
|
768
620
|
try:
|
|
769
621
|
self.type = self.instance.get_message_type(data)
|
|
770
|
-
except:
|
|
622
|
+
except Exception:
|
|
771
623
|
self.type = "text"
|
|
772
624
|
self.data = data
|
|
773
625
|
self.rec = rec_type
|
|
774
|
-
|
|
626
|
+
try:
|
|
627
|
+
self.to = to if to != "" else self.instance.get_mobile(data)
|
|
628
|
+
except Exception:
|
|
629
|
+
self.to = to
|
|
630
|
+
try:
|
|
631
|
+
self.sender = self.instance.get_author(data) or self.to
|
|
632
|
+
except Exception:
|
|
633
|
+
self.sender = self.to
|
|
775
634
|
try:
|
|
776
635
|
self.content = content if content != "" else self.instance.get_message(data)
|
|
777
|
-
except:
|
|
636
|
+
except Exception:
|
|
778
637
|
self.content = content
|
|
779
638
|
try:
|
|
780
639
|
self.name = self.instance.get_name(data)
|
|
781
|
-
except:
|
|
640
|
+
except Exception:
|
|
782
641
|
self.name = None
|
|
783
642
|
|
|
784
643
|
if self.type == "image":
|
|
785
644
|
try:
|
|
786
645
|
self.image = self.instance.get_image(data)
|
|
787
|
-
except:
|
|
646
|
+
except Exception:
|
|
788
647
|
self.image = None
|
|
789
648
|
if self.type == "sticker":
|
|
790
649
|
try:
|
|
791
650
|
self.sticker = self.instance.get_sticker(data)
|
|
792
|
-
except:
|
|
651
|
+
except Exception:
|
|
793
652
|
self.sticker = None
|
|
794
653
|
elif self.type == "video":
|
|
795
654
|
try:
|
|
796
655
|
self.video = self.instance.get_video(data)
|
|
797
|
-
except:
|
|
656
|
+
except Exception:
|
|
798
657
|
self.video = None
|
|
799
658
|
elif self.type == "audio":
|
|
800
659
|
try:
|
|
801
660
|
self.audio = self.instance.get_audio(data)
|
|
802
|
-
except:
|
|
661
|
+
except Exception:
|
|
803
662
|
pass
|
|
804
663
|
elif self.type == "document":
|
|
805
664
|
try:
|
|
806
665
|
self.document = self.instance.get_document(data)
|
|
807
|
-
except:
|
|
666
|
+
except Exception:
|
|
808
667
|
pass
|
|
809
668
|
elif self.type == "location":
|
|
810
669
|
try:
|
|
811
670
|
self.location = self.instance.get_location(data)
|
|
812
|
-
except:
|
|
671
|
+
except Exception:
|
|
813
672
|
pass
|
|
814
673
|
elif self.type == "interactive":
|
|
815
674
|
try:
|
|
816
675
|
self.interactive = self.instance.get_interactive_response(data)
|
|
817
|
-
except:
|
|
676
|
+
except Exception:
|
|
818
677
|
pass
|
|
678
|
+
elif self.type == "reaction":
|
|
679
|
+
try:
|
|
680
|
+
self.reaction = self.instance.get_reaction(data)
|
|
681
|
+
except Exception:
|
|
682
|
+
self.reaction = None
|
|
819
683
|
|
|
820
684
|
async def reply(
|
|
821
685
|
self, reply_text: str = "", preview_url: bool = True
|
|
@@ -847,20 +711,28 @@ class AsyncMessage:
|
|
|
847
711
|
f"Message not sent to {self.instance.get_author(self.data)}"
|
|
848
712
|
)
|
|
849
713
|
logging.info(f"Status code: {response.status}")
|
|
850
|
-
logging.error(
|
|
714
|
+
logging.error("Request failed (status=%s)", response.status)
|
|
851
715
|
return await response.json()
|
|
852
716
|
|
|
853
717
|
f = asyncio.ensure_future(call())
|
|
854
718
|
await asyncio.sleep(0.001) # make asyncio run the task
|
|
855
719
|
return f
|
|
856
720
|
|
|
857
|
-
async def mark_as_read(self) -> asyncio.Future:
|
|
721
|
+
async def mark_as_read(self, typing: bool = False) -> asyncio.Future:
|
|
722
|
+
"""
|
|
723
|
+
Mark this inbound message as read.
|
|
858
724
|
|
|
859
|
-
|
|
725
|
+
Args:
|
|
726
|
+
typing: When True, also display a typing indicator (dismissed after
|
|
727
|
+
a reply or ~25 seconds). See Meta typing-indicators docs.
|
|
728
|
+
"""
|
|
729
|
+
payload: dict[str, Any] = {
|
|
860
730
|
"messaging_product": "whatsapp",
|
|
861
731
|
"status": "read",
|
|
862
732
|
"message_id": self.id,
|
|
863
733
|
}
|
|
734
|
+
if typing:
|
|
735
|
+
payload["typing_indicator"] = {"type": "text"}
|
|
864
736
|
|
|
865
737
|
async def call():
|
|
866
738
|
async with aiohttp.ClientSession() as session:
|
|
@@ -868,24 +740,27 @@ class AsyncMessage:
|
|
|
868
740
|
f"{self.instance.url}", headers=self.instance.headers, json=payload
|
|
869
741
|
) as response:
|
|
870
742
|
if response.status == 200:
|
|
871
|
-
logging.info(
|
|
872
|
-
return await response.json()
|
|
873
|
-
else:
|
|
874
|
-
logging.error(await response.json())
|
|
743
|
+
logging.info("Request succeeded (status=%s)", response.status)
|
|
875
744
|
return await response.json()
|
|
745
|
+
logging.error("Request failed (status=%s)", response.status)
|
|
746
|
+
return await response.json()
|
|
876
747
|
|
|
877
748
|
f = asyncio.ensure_future(call())
|
|
878
749
|
await asyncio.sleep(0.001) # make asyncio run the task
|
|
879
750
|
return f
|
|
880
751
|
|
|
752
|
+
async def show_typing(self) -> asyncio.Future:
|
|
753
|
+
"""Mark as read and show a typing indicator while preparing a reply."""
|
|
754
|
+
return await self.mark_as_read(typing=True)
|
|
755
|
+
|
|
881
756
|
async def send(self, sender=None, preview_url: bool = True) -> asyncio.Future:
|
|
882
757
|
try:
|
|
883
758
|
sender = dict(self.instance.l)[sender]
|
|
884
759
|
|
|
885
|
-
except:
|
|
760
|
+
except Exception:
|
|
886
761
|
sender = self.instance.phone_number_id
|
|
887
762
|
|
|
888
|
-
if sender
|
|
763
|
+
if sender is None:
|
|
889
764
|
sender = self.instance.phone_number_id
|
|
890
765
|
|
|
891
766
|
url = f"https://graph.facebook.com/{self.instance.LATEST}/{sender}/messages"
|
|
@@ -899,7 +774,6 @@ class AsyncMessage:
|
|
|
899
774
|
logging.info(f"Sending message to {self.to}")
|
|
900
775
|
|
|
901
776
|
async def call():
|
|
902
|
-
print("sending")
|
|
903
777
|
async with aiohttp.ClientSession() as session:
|
|
904
778
|
async with session.post(
|
|
905
779
|
url, headers=self.headers, json=data
|
|
@@ -909,7 +783,7 @@ class AsyncMessage:
|
|
|
909
783
|
return await response.json()
|
|
910
784
|
logging.info(f"Message not sent to {self.to}")
|
|
911
785
|
logging.info(f"Status code: {response.status}")
|
|
912
|
-
logging.error(
|
|
786
|
+
logging.error("Request failed (status=%s)", response.status)
|
|
913
787
|
return await response.json()
|
|
914
788
|
|
|
915
789
|
f = asyncio.ensure_future(call())
|
|
@@ -936,7 +810,7 @@ class AsyncMessage:
|
|
|
936
810
|
return await response.json()
|
|
937
811
|
logging.info(f"Reaction not sent to {self.to}")
|
|
938
812
|
logging.info(f"Status code: {response.status}")
|
|
939
|
-
logging.debug(
|
|
813
|
+
logging.debug("Request failed (status=%s)", response.status)
|
|
940
814
|
return await response.json()
|
|
941
815
|
|
|
942
816
|
f = asyncio.ensure_future(call())
|