whatsapp-python 4.2.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 CHANGED
@@ -1,45 +1,178 @@
1
1
  """
2
2
  Unofficial Python wrapper for the WhatsApp Cloud API.
3
3
  """
4
+
4
5
  from __future__ import annotations
5
- import requests
6
- import logging
7
- import json
6
+
8
7
  import asyncio
8
+ import logging
9
+ from typing import Any, Callable, Optional, Union
10
+
9
11
  import aiohttp
10
- from bs4 import BeautifulSoup
12
+ import requests
11
13
  from fastapi import FastAPI, HTTPException, Request
12
14
  from uvicorn import run as _run
15
+
13
16
  from .constants import VERSION
17
+ from .errors import Handle
18
+ from .graph_api import resolve_graph_api_version
14
19
  from .ext._property import authorized
15
20
  from .ext._send_others import send_custom_json, send_contacts
16
21
  from .ext._message import send_template
17
- from .ext._send_media import send_image, send_video, send_audio, send_location, send_sticker, send_document
22
+ from .ext._send_media import (
23
+ send_image,
24
+ send_video,
25
+ send_audio,
26
+ send_location,
27
+ send_sticker,
28
+ send_document,
29
+ )
18
30
  from .ext._media import upload_media, query_media_url, download_media, delete_media
19
- from .ext._buttons import send_button, create_button, send_reply_button
20
- from .ext._static import is_message, get_mobile, get_author, get_name, get_message, get_message_id, get_message_type, get_message_timestamp, get_audio, get_delivery, get_document, get_image, get_sticker, get_interactive_response, get_location, get_video, changed_field
31
+ from .ext._buttons import (
32
+ send_button,
33
+ create_button,
34
+ send_reply_button,
35
+ send_cta_url,
36
+ send_location_request,
37
+ )
38
+ from .ext._static import (
39
+ is_message,
40
+ get_mobile,
41
+ get_author,
42
+ get_name,
43
+ get_message,
44
+ get_message_id,
45
+ get_message_type,
46
+ get_message_timestamp,
47
+ get_audio,
48
+ get_delivery,
49
+ get_document,
50
+ get_image,
51
+ get_sticker,
52
+ get_interactive_response,
53
+ get_location,
54
+ get_video,
55
+ get_reaction,
56
+ changed_field,
57
+ )
21
58
 
22
59
  from .async_ext._property import authorized as async_authorized
23
- from .async_ext._send_others import send_custom_json as async_send_custom_json, send_contacts as async_send_contacts
60
+ from .async_ext._send_others import (
61
+ send_custom_json as async_send_custom_json,
62
+ send_contacts as async_send_contacts,
63
+ )
24
64
  from .async_ext._message import send_template as async_send_template
25
- from .async_ext._send_media import send_image as async_send_image, send_video as async_send_video, send_audio as async_send_audio, send_location as async_send_location, send_sticker as async_send_sticker, send_document as async_send_document
26
- from .async_ext._media import upload_media as async_upload_media, query_media_url as async_query_media_url, download_media as async_download_media, delete_media as async_delete_media
27
- from .async_ext._buttons import send_button as async_send_button, create_button as async_create_button, send_reply_button as async_send_reply_button
28
- from .async_ext._static import is_message as async_is_message, get_mobile as async_get_mobile, get_author as async_get_author, get_name as async_get_name, get_message as async_get_message, get_message_id as async_get_message_id, get_message_type as async_get_message_type, get_message_timestamp as async_get_message_timestamp, get_audio as async_get_audio, get_delivery as async_get_delivery, get_document as async_get_document, get_image as async_get_image, get_sticker as async_get_sticker, get_interactive_response as async_get_interactive_response, get_location as async_get_location, get_video as async_get_video, changed_field as async_changed_field
29
-
30
-
31
- class WhatsApp(object):
32
- def __init__(self, token: str = "", phone_number_id: dict = "", logger: bool = True, update_check: bool = True, verify_token: str = "", debug: bool = True, version: str = "latest"):
65
+ from .async_ext._send_media import (
66
+ send_image as async_send_image,
67
+ send_video as async_send_video,
68
+ send_audio as async_send_audio,
69
+ send_location as async_send_location,
70
+ send_sticker as async_send_sticker,
71
+ send_document as async_send_document,
72
+ )
73
+ from .async_ext._media import (
74
+ upload_media as async_upload_media,
75
+ query_media_url as async_query_media_url,
76
+ download_media as async_download_media,
77
+ delete_media as async_delete_media,
78
+ )
79
+ from .async_ext._buttons import (
80
+ send_button as async_send_button,
81
+ create_button as async_create_button,
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,
85
+ )
86
+ from .async_ext._static import (
87
+ is_message as async_is_message,
88
+ get_mobile as async_get_mobile,
89
+ get_author as async_get_author,
90
+ get_name as async_get_name,
91
+ get_message as async_get_message,
92
+ get_message_id as async_get_message_id,
93
+ get_message_type as async_get_message_type,
94
+ get_message_timestamp as async_get_message_timestamp,
95
+ get_audio as async_get_audio,
96
+ get_delivery as async_get_delivery,
97
+ get_document as async_get_document,
98
+ get_image as async_get_image,
99
+ get_sticker as async_get_sticker,
100
+ get_interactive_response as async_get_interactive_response,
101
+ get_location as async_get_location,
102
+ get_video as async_get_video,
103
+ get_reaction as async_get_reaction,
104
+ changed_field as async_changed_field,
105
+ )
106
+
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
+ )
150
+
151
+
152
+ class WhatsApp:
153
+ def __init__(
154
+ self,
155
+ token: str = "",
156
+ phone_number_id: Union[dict, str] = "",
157
+ logger: bool = True,
158
+ update_check: bool = True,
159
+ verify_token: str = "",
160
+ debug: bool = True,
161
+ version: str = "latest",
162
+ ):
33
163
  """
34
164
  Initialize the WhatsApp Object
35
165
 
36
166
  Args:
37
167
  token[str]: Token for the WhatsApp cloud API obtained from the Facebook developer portal
38
- phone_number_id[str]: Phone number id for the WhatsApp cloud API obtained from the developer portal
168
+ phone_number_id[dict]: Mapping of friendly keys to phone number IDs from the developer portal
39
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\"``)
40
174
  """
41
175
 
42
- # Check if the version is up to date
43
176
  logging.getLogger(__name__).addHandler(logging.NullHandler())
44
177
 
45
178
  self.l = phone_number_id
@@ -47,70 +180,30 @@ class WhatsApp(object):
47
180
  if isinstance(phone_number_id, dict):
48
181
  # use first phone number id as default
49
182
  phone_number_id = phone_number_id[list(phone_number_id.keys())[0]]
50
-
51
183
  elif phone_number_id == "":
52
184
  logging.error("Phone number ID not provided")
53
- raise ValueError(
54
- "Phone number ID not provided but required")
185
+ raise ValueError("Phone number ID not provided but required")
55
186
  elif isinstance(phone_number_id, str):
56
187
  logging.critical(
57
- "The phone number ID should be a dictionary of phone numbers and their IDs. Using strings is deprecated.")
188
+ "The phone number ID should be a dictionary of phone numbers and their IDs. Using strings is deprecated."
189
+ )
58
190
  raise ValueError(
59
- "Phone number ID should be a dictionary of phone numbers and their IDs")
60
- else:
61
- pass
191
+ "Phone number ID should be a dictionary of phone numbers and their IDs"
192
+ )
62
193
 
63
- self.VERSION = VERSION # package version
64
- r = requests.get("https://developers.facebook.com/docs/graph-api/changelog/").text
65
-
66
- # dynamically get the latest version of the API
67
- if version == "latest":
68
- soup = BeautifulSoup(r, features="html.parser")
69
- t1 = soup.findAll("table")
70
-
71
- def makeversion(table):
72
- result = []
73
- allrows = table.findAll('tr')
74
- for row in allrows:
75
- result.append([])
76
- allcols = row.findAll('td')
77
- for col in allcols:
78
- thestrings = [(s) for s in col.findAll(text=True)]
79
- thetext = ''.join(thestrings)
80
- result[-1].append(thetext)
81
- return result[0][1]
82
- self.LATEST = makeversion(t1[0]) # latest version of the API
83
- else:
84
- self.LATEST = version
194
+ self.VERSION = VERSION
195
+ self.LATEST = resolve_graph_api_version(version)
85
196
 
86
197
  if update_check is True:
87
- latest = str(requests.get(
88
- "https://pypi.org/pypi/whatsapp-python/json").json()["info"]["version"])
89
- if self.VERSION != latest:
90
- try:
91
- version_int = int(self.VERSION.replace(".", ""))
92
- except:
93
- version_int = 0
94
- try:
95
- latest_int = int(latest.replace(".", ""))
96
- except:
97
- latest_int = 0
98
- # 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)
99
- if version_int < latest_int:
100
- if version_int == 0:
101
- logging.critical(
102
- 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.")
103
- else:
104
- logging.critical(
105
- 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.")
198
+ _check_package_update(self.VERSION)
106
199
 
107
200
  if token == "":
108
201
  logging.error("Token not provided")
109
202
  raise ValueError("Token not provided but required")
110
203
  if phone_number_id == "":
111
204
  logging.error("Phone number ID not provided")
112
- raise ValueError(
113
- "Phone number ID not provided but required")
205
+ raise ValueError("Phone number ID not provided but required")
206
+
114
207
  self.token = token
115
208
  self.phone_number_id = phone_number_id
116
209
  self.base_url = f"https://graph.facebook.com/{self.LATEST}"
@@ -119,12 +212,13 @@ class WhatsApp(object):
119
212
 
120
213
  async def base(*args):
121
214
  pass
215
+
122
216
  self.message_handler = base
123
217
  self.other_handler = base
124
218
  self.verification_handler = base
125
219
  self.headers = {
126
220
  "Content-Type": "application/json",
127
- "Authorization": f"Bearer {self.token}"
221
+ "Authorization": f"Bearer {self.token}",
128
222
  }
129
223
  if logger is False:
130
224
  logging.disable(logging.INFO)
@@ -157,24 +251,23 @@ class WhatsApp(object):
157
251
  data = await r.json()
158
252
  if data is None:
159
253
  return {"success": False}
160
- data_str = json.dumps(data, indent=4)
161
- # log the data received only if the log level is debug
162
- logging.debug(f"Received webhook data: {data_str}")
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")
163
257
 
164
- changed_field = self.changed_field(data)
165
- if changed_field == "messages":
258
+ changed = self.changed_field(data)
259
+ if changed == "messages":
166
260
  new_message = self.is_message(data)
167
261
  if new_message:
168
- msg = Message(instance=self, data=data)
262
+ msg = self.create_message(data=data)
169
263
  await self.message_handler(msg)
170
264
  await self.other_handler(msg)
171
265
  return {"success": True}
172
266
  except Exception as e:
173
267
  logging.error(f"Error parsing message: {e}")
174
- raise HTTPException(status_code=500, detail={
175
- "success": False,
176
- "error": str(e)
177
- })
268
+ raise HTTPException(
269
+ status_code=500, detail={"success": False, "error": str(e)}
270
+ )
178
271
 
179
272
  # all the files starting with _ are imported here, and should not be imported directly.
180
273
 
@@ -193,12 +286,15 @@ class WhatsApp(object):
193
286
  get_interactive_response = staticmethod(get_interactive_response)
194
287
  get_location = staticmethod(get_location)
195
288
  get_video = staticmethod(get_video)
289
+ get_reaction = staticmethod(get_reaction)
196
290
  changed_field = staticmethod(changed_field)
197
291
  get_author = staticmethod(get_author)
198
292
 
199
293
  send_button = send_button
200
294
  create_button = create_button
201
295
  send_reply_button = send_reply_button
296
+ send_cta_url = send_cta_url
297
+ send_location_request = send_location_request
202
298
  send_image = send_image
203
299
  send_video = send_video
204
300
  send_audio = send_audio
@@ -214,8 +310,7 @@ class WhatsApp(object):
214
310
  send_contacts = send_contacts
215
311
  authorized = property(authorized)
216
312
 
217
-
218
- def create_message(self, **kwargs) -> Message:
313
+ def create_message(self, **kwargs) -> "Message":
219
314
  """
220
315
  Create a message object
221
316
 
@@ -227,7 +322,7 @@ class WhatsApp(object):
227
322
  """
228
323
  return Message(**kwargs, instance=self)
229
324
 
230
- def on_message(self, handler: function):
325
+ def on_message(self, handler: Callable[..., Any]):
231
326
  """
232
327
  Set the handler for incoming messages
233
328
 
@@ -236,7 +331,7 @@ class WhatsApp(object):
236
331
  """
237
332
  self.message_handler = handler
238
333
 
239
- def on_event(self, handler: function):
334
+ def on_event(self, handler: Callable[..., Any]):
240
335
  """
241
336
  Set the handler for other events
242
337
 
@@ -245,7 +340,7 @@ class WhatsApp(object):
245
340
  """
246
341
  self.other_handler = handler
247
342
 
248
- def on_verification(self, handler: function):
343
+ def on_verification(self, handler: Callable[..., Any]):
249
344
  """
250
345
  Set the handler for verification
251
346
 
@@ -259,176 +354,32 @@ class WhatsApp(object):
259
354
 
260
355
 
261
356
  class AsyncWhatsApp(WhatsApp):
262
- def __init__(self, token: str = "", phone_number_id: dict = "", logger: bool = True, update_check: bool = True, verify_token: str = "", debug: bool = True, version: str = "latest"):
263
- """
264
- Initialize the WhatsApp Object
265
-
266
- Args:
267
- token[str]: Token for the WhatsApp cloud API obtained from the Facebook developer portal
268
- phone_number_id[str]: Phone number id for the WhatsApp cloud API obtained from the developer portal
269
- logger[bool]: Whether to enable logging or not (default: True)
270
- """
271
-
272
- # Check if the version is up to date
273
- logging.getLogger(__name__).addHandler(logging.NullHandler())
274
-
275
- self.l = phone_number_id
276
-
277
- if isinstance(phone_number_id, dict):
278
- # use first phone number id as default
279
- phone_number_id = phone_number_id[list(phone_number_id.keys())[0]]
280
-
281
- elif phone_number_id == "":
282
- logging.error("Phone number ID not provided")
283
- raise ValueError(
284
- "Phone number ID not provided but required")
285
- elif isinstance(phone_number_id, str):
286
- logging.critical(
287
- "The phone number ID should be a dictionary of phone numbers and their IDs. Using strings is deprecated.")
288
- raise ValueError(
289
- "Phone number ID should be a dictionary of phone numbers and their IDs")
290
- else:
291
- pass
292
-
293
- self.VERSION = VERSION # package version
294
- r = requests.get("https://developers.facebook.com/docs/graph-api/changelog/").text
295
-
296
- # dynamically get the latest version of the API
297
- if version == "latest":
298
- soup = BeautifulSoup(r, features="html.parser")
299
- t1 = soup.findAll("table")
300
-
301
- def makeversion(table):
302
- result = []
303
- allrows = table.findAll('tr')
304
- for row in allrows:
305
- result.append([])
306
- allcols = row.findAll('td')
307
- for col in allcols:
308
- thestrings = [(s) for s in col.findAll(text=True)]
309
- thetext = ''.join(thestrings)
310
- result[-1].append(thetext)
311
- return result[0][1]
312
- self.LATEST = makeversion(t1[0]) # latest version of the API
313
- else:
314
- self.LATEST = version
315
-
316
- if update_check is True:
317
- latest = str(requests.get(
318
- "https://pypi.org/pypi/whatsapp-python/json").json()["info"]["version"])
319
- if self.VERSION != latest:
320
- try:
321
- version_int = int(self.VERSION.replace(".", ""))
322
- except:
323
- version_int = 0
324
- try:
325
- latest_int = int(latest.replace(".", ""))
326
- except:
327
- latest_int = 0
328
- # 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)
329
- if version_int < latest_int:
330
- if version_int == 0:
331
- logging.critical(
332
- 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.")
333
- else:
334
- logging.critical(
335
- 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.")
336
-
337
- if token == "":
338
- logging.error("Token not provided")
339
- raise ValueError("Token not provided but required")
340
- if phone_number_id == "":
341
- logging.error("Phone number ID not provided")
342
- raise ValueError(
343
- "Phone number ID not provided but required")
344
- self.token = token
345
- self.phone_number_id = phone_number_id
346
- self.base_url = f"https://graph.facebook.com/{self.LATEST}"
347
- self.url = f"{self.base_url}/{phone_number_id}/messages"
348
- self.verify_token = verify_token
349
-
350
- async def base(*args):
351
- pass
352
- self.message_handler = base
353
- self.other_handler = base
354
- self.verification_handler = base
355
- self.headers = {
356
- "Content-Type": "application/json",
357
- "Authorization": f"Bearer {self.token}"
358
- }
359
- if logger is False:
360
- logging.disable(logging.INFO)
361
- logging.disable(logging.ERROR)
362
- if debug is False:
363
- logging.disable(logging.DEBUG)
364
- logging.disable(logging.ERROR)
365
-
366
- self.app = FastAPI()
367
-
368
- # Verification handler has 1 argument: challenge (str | bool): str if verification is successful, False if not
369
-
370
- @self.app.get("/")
371
- async def verify_endpoint(r: Request):
372
- if r.query_params.get("hub.verify_token") == self.verify_token:
373
- logging.debug("Webhook verified successfully")
374
- challenge = r.query_params.get("hub.challenge")
375
- await self.verification_handler(challenge)
376
- await self.other_handler(challenge)
377
- return int(challenge)
378
- logging.error("Webhook Verification failed - token mismatch")
379
- await self.verification_handler(False)
380
- await self.other_handler(False)
381
- return {"success": False}
382
-
383
- @self.app.post("/")
384
- async def hook(r: Request):
385
- try:
386
- # Handle Webhook Subscriptions
387
- data = await r.json()
388
- if data is None:
389
- return {"success": False}
390
- data_str = json.dumps(data, indent=4)
391
- # log the data received only if the log level is debug
392
- logging.debug(f"Received webhook data: {data_str}")
393
-
394
- changed_field = self.changed_field(data)
395
- if changed_field == "messages":
396
- new_message = self.is_message(data)
397
- if new_message:
398
- msg = Message(instance=self, data=data)
399
- await self.message_handler(msg)
400
- await self.other_handler(msg)
401
- return {"success": True}
402
- except Exception as e:
403
- logging.error(f"Error parsing message: {e}")
404
- raise HTTPException(status_code=500, detail={
405
- "success": False,
406
- "error": str(e)
407
- })
408
-
409
- # all the files starting with _ are imported here, and should not be imported directly.
410
-
411
- is_message = staticmethod(is_message)
412
- get_mobile = staticmethod(get_mobile)
413
- get_name = staticmethod(get_name)
414
- get_message = staticmethod(get_message)
415
- get_message_id = staticmethod(get_message_id)
416
- get_message_type = staticmethod(get_message_type)
417
- get_message_timestamp = staticmethod(get_message_timestamp)
418
- get_audio = staticmethod(get_audio)
419
- get_delivery = staticmethod(get_delivery)
420
- get_document = staticmethod(get_document)
421
- get_image = staticmethod(get_image)
422
- get_sticker = staticmethod(get_sticker)
423
- get_interactive_response = staticmethod(get_interactive_response)
424
- get_location = staticmethod(get_location)
425
- get_video = staticmethod(get_video)
426
- changed_field = staticmethod(changed_field)
427
- 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)
428
377
 
429
378
  send_button = async_send_button
430
379
  create_button = async_create_button
431
380
  send_reply_button = async_send_reply_button
381
+ send_cta_url = async_send_cta_url
382
+ send_location_request = async_send_location_request
432
383
  send_image = async_send_image
433
384
  send_video = async_send_video
434
385
  send_audio = async_send_audio
@@ -443,12 +394,13 @@ class AsyncWhatsApp(WhatsApp):
443
394
  send_custom_json = async_send_custom_json
444
395
  send_contacts = async_send_contacts
445
396
  authorized = property(async_authorized)
446
-
447
397
 
448
-
449
- def create_message(self, **kwargs) -> AsyncMessage:
398
+ def handle(self, data: dict):
399
+ return Handle(data)
400
+
401
+ def create_message(self, **kwargs) -> "AsyncMessage":
450
402
  """
451
- Create a message object
403
+ Create an async message object
452
404
 
453
405
  Args:
454
406
  data[dict]: The message data
@@ -458,101 +410,93 @@ class AsyncWhatsApp(WhatsApp):
458
410
  """
459
411
  return AsyncMessage(**kwargs, instance=self)
460
412
 
461
- def on_message(self, handler: function):
462
- """
463
- Set the handler for incoming messages
464
413
 
465
- Args:
466
- handler[function]: The handler function
467
- """
468
- self.message_handler = handler
469
-
470
- def on_event(self, handler: function):
471
- """
472
- Set the handler for other events
473
-
474
- Args:
475
- handler[function]: The handler function
476
- """
477
- self.other_handler = handler
478
-
479
- def on_verification(self, handler: function):
480
- """
481
- Set the handler for verification
482
-
483
- Args:
484
- handler[function]: The handler function
485
- """
486
- self.verification_handler = handler
487
-
488
- def run(self, host: str = "localhost", port: int = 5000, **options):
489
- _run(self.app, host=host, port=port, **options)
490
-
491
-
492
-
493
- class Message(object):
414
+ class Message:
494
415
  # type: ignore
495
- def __init__(self, id: int = None, data: dict = {}, instance: WhatsApp = None, content: str = "", to: str = "", rec_type: str = "individual"):
416
+ def __init__(
417
+ self,
418
+ id: Optional[str] = None,
419
+ data: Optional[dict] = None,
420
+ instance: Optional[WhatsApp] = None,
421
+ content: str = "",
422
+ to: str = "",
423
+ rec_type: str = "individual",
424
+ ):
425
+ data = data or {}
496
426
  self.instance = instance
497
427
  self.url = self.instance.url
498
428
  self.headers = self.instance.headers
499
429
 
500
430
  try:
501
431
  self.id = instance.get_message_id(data)
502
- except:
432
+ except Exception:
503
433
  self.id = id
504
434
  try:
505
435
  self.type = self.instance.get_message_type(data)
506
- except:
436
+ except Exception:
507
437
  self.type = "text"
508
438
  self.data = data
509
439
  self.rec = rec_type
510
- self.to = to
440
+
441
+ try:
442
+ self.to = to if to != "" else self.instance.get_mobile(data)
443
+ except Exception:
444
+ self.to = to
445
+
511
446
  try:
512
- self.content = content if content != "" else self.instance.get_message(
513
- data)
514
- except:
447
+ self.sender = self.instance.get_author(data) or self.to
448
+ except Exception:
449
+ self.sender = self.to
450
+
451
+ try:
452
+ self.content = content if content != "" else self.instance.get_message(data)
453
+ except Exception:
515
454
  self.content = content
516
455
  try:
517
456
  self.name = self.instance.get_name(data)
518
- except:
457
+ except Exception:
519
458
  self.name = None
520
459
 
521
460
  if self.type == "image":
522
461
  try:
523
462
  self.image = self.instance.get_image(data)
524
- except:
463
+ except Exception:
525
464
  self.image = None
526
465
  if self.type == "sticker":
527
466
  try:
528
467
  self.sticker = self.instance.get_sticker(data)
529
- except:
468
+ except Exception:
530
469
  self.sticker = None
531
470
  elif self.type == "video":
532
471
  try:
533
472
  self.video = self.instance.get_video(data)
534
- except:
473
+ except Exception:
535
474
  self.video = None
536
475
  elif self.type == "audio":
537
476
  try:
538
477
  self.audio = self.instance.get_audio(data)
539
- except:
478
+ except Exception:
540
479
  pass
541
480
  elif self.type == "document":
542
481
  try:
543
482
  self.document = self.instance.get_document(data)
544
- except:
483
+ except Exception:
545
484
  pass
546
485
  elif self.type == "location":
547
486
  try:
548
487
  self.location = self.instance.get_location(data)
549
- except:
488
+ except Exception:
550
489
  pass
551
490
  elif self.type == "interactive":
552
491
  try:
553
492
  self.interactive = self.instance.get_interactive_response(data)
554
- except:
493
+ except Exception:
555
494
  pass
495
+ elif self.type == "reaction":
496
+ try:
497
+ self.reaction = self.instance.get_reaction(data)
498
+ except Exception:
499
+ self.reaction = None
556
500
 
557
501
  def reply(self, reply_text: str = "", preview_url: bool = True) -> dict:
558
502
  if self.data == {}:
@@ -573,35 +517,47 @@ class Message(object):
573
517
  return r.json()
574
518
  logging.info(f"Message not sent to {self.instance.get_author(self.data)}")
575
519
  logging.info(f"Status code: {r.status_code}")
576
- logging.error(f"Response: {r.json()}")
520
+ logging.error("Request failed (status=%s)", r.status_code)
577
521
  return r.json()
578
522
 
579
- def mark_as_read(self) -> dict:
523
+ def mark_as_read(self, typing: bool = False) -> dict:
524
+ """
525
+ Mark this inbound message as read.
580
526
 
581
- payload = {
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] = {
582
532
  "messaging_product": "whatsapp",
583
533
  "status": "read",
584
534
  "message_id": self.id,
585
535
  }
536
+ if typing:
537
+ payload["typing_indicator"] = {"type": "text"}
586
538
 
587
539
  response = requests.post(
588
- f"{self.instance.url}", headers=self.instance.headers, json=payload)
540
+ f"{self.instance.url}", headers=self.instance.headers, json=payload
541
+ )
589
542
  if response.status_code == 200:
590
- logging.info(response.json())
591
- return response.json()
592
- else:
593
- logging.error(response.json())
543
+ logging.info("Request succeeded (status=%s)", response.status_code)
594
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)
595
551
 
596
552
  def send(self, sender=None, preview_url: bool = True) -> dict:
597
553
 
598
554
  try:
599
555
  sender = dict(self.instance.l)[sender]
600
556
 
601
- except:
557
+ except Exception:
602
558
  sender = self.instance.phone_number_id
603
559
 
604
- if sender == None:
560
+ if sender is None:
605
561
  sender = self.instance.phone_number_id
606
562
 
607
563
  url = f"https://graph.facebook.com/{self.instance.LATEST}/{sender}/messages"
@@ -619,7 +575,7 @@ class Message(object):
619
575
  return r.json()
620
576
  logging.info(f"Message not sent to {self.to}")
621
577
  logging.info(f"Status code: {r.status_code}")
622
- logging.error(f"Response: {r.json()}")
578
+ logging.error("Request failed (status=%s)", r.status_code)
623
579
  return r.json()
624
580
 
625
581
  def react(self, emoji: str) -> dict:
@@ -637,77 +593,97 @@ class Message(object):
637
593
  return r.json()
638
594
  logging.info(f"Reaction not sent to {self.to}")
639
595
  logging.info(f"Status code: {r.status_code}")
640
- logging.debug(f"Response: {r.json()}")
596
+ logging.debug("Request failed (status=%s)", r.status_code)
641
597
  return r.json()
642
598
 
643
599
 
644
-
645
- class AsyncMessage(object):
600
+ class AsyncMessage:
646
601
  # type: ignore
647
- def __init__(self, id: int = None, data: dict = {}, instance: WhatsApp = None, content: str = "", to: str = "", rec_type: str = "individual"):
602
+ def __init__(
603
+ self,
604
+ id: Optional[str] = None,
605
+ data: Optional[dict] = None,
606
+ instance: Optional[WhatsApp] = None,
607
+ content: str = "",
608
+ to: str = "",
609
+ rec_type: str = "individual",
610
+ ):
611
+ data = data or {}
648
612
  self.instance = instance
649
613
  self.url = self.instance.url
650
614
  self.headers = self.instance.headers
651
615
 
652
616
  try:
653
617
  self.id = instance.get_message_id(data)
654
- except:
618
+ except Exception:
655
619
  self.id = id
656
620
  try:
657
621
  self.type = self.instance.get_message_type(data)
658
- except:
622
+ except Exception:
659
623
  self.type = "text"
660
624
  self.data = data
661
625
  self.rec = rec_type
662
- self.to = to
663
626
  try:
664
- self.content = content if content != "" else self.instance.get_message(
665
- data)
666
- except:
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
634
+ try:
635
+ self.content = content if content != "" else self.instance.get_message(data)
636
+ except Exception:
667
637
  self.content = content
668
638
  try:
669
639
  self.name = self.instance.get_name(data)
670
- except:
640
+ except Exception:
671
641
  self.name = None
672
642
 
673
643
  if self.type == "image":
674
644
  try:
675
645
  self.image = self.instance.get_image(data)
676
- except:
646
+ except Exception:
677
647
  self.image = None
678
648
  if self.type == "sticker":
679
649
  try:
680
650
  self.sticker = self.instance.get_sticker(data)
681
- except:
651
+ except Exception:
682
652
  self.sticker = None
683
653
  elif self.type == "video":
684
654
  try:
685
655
  self.video = self.instance.get_video(data)
686
- except:
656
+ except Exception:
687
657
  self.video = None
688
658
  elif self.type == "audio":
689
659
  try:
690
660
  self.audio = self.instance.get_audio(data)
691
- except:
661
+ except Exception:
692
662
  pass
693
663
  elif self.type == "document":
694
664
  try:
695
665
  self.document = self.instance.get_document(data)
696
- except:
666
+ except Exception:
697
667
  pass
698
668
  elif self.type == "location":
699
669
  try:
700
670
  self.location = self.instance.get_location(data)
701
- except:
671
+ except Exception:
702
672
  pass
703
673
  elif self.type == "interactive":
704
674
  try:
705
675
  self.interactive = self.instance.get_interactive_response(data)
706
- except:
676
+ except Exception:
707
677
  pass
708
-
678
+ elif self.type == "reaction":
679
+ try:
680
+ self.reaction = self.instance.get_reaction(data)
681
+ except Exception:
682
+ self.reaction = None
709
683
 
710
- async def reply(self, reply_text: str = "", preview_url: bool = True) -> asyncio.Future:
684
+ async def reply(
685
+ self, reply_text: str = "", preview_url: bool = True
686
+ ) -> asyncio.Future:
711
687
  if self.data == {}:
712
688
  return {"error": "No data provided"}
713
689
  author = self.instance.get_author(self.data)
@@ -720,49 +696,71 @@ class AsyncMessage(object):
720
696
  "text": {"preview_url": preview_url, "body": reply_text},
721
697
  }
722
698
  logging.info(f"Replying to {self.id}")
699
+
723
700
  async def call():
724
701
  async with aiohttp.ClientSession() as session:
725
- async with session.post(self.url, headers=self.headers, json=payload) as response:
702
+ async with session.post(
703
+ self.url, headers=self.headers, json=payload
704
+ ) as response:
726
705
  if response.status == 200:
727
- logging.info(f"Message sent to {self.instance.get_author(self.data)}")
706
+ logging.info(
707
+ f"Message sent to {self.instance.get_author(self.data)}"
708
+ )
728
709
  return await response.json()
729
- logging.info(f"Message not sent to {self.instance.get_author(self.data)}")
710
+ logging.info(
711
+ f"Message not sent to {self.instance.get_author(self.data)}"
712
+ )
730
713
  logging.info(f"Status code: {response.status}")
731
- logging.error(f"Response: {await response.json()}")
714
+ logging.error("Request failed (status=%s)", response.status)
732
715
  return await response.json()
716
+
733
717
  f = asyncio.ensure_future(call())
734
- await asyncio.sleep(.001) # make asyncio run the task
718
+ await asyncio.sleep(0.001) # make asyncio run the task
735
719
  return f
736
-
737
-
738
- async def mark_as_read(self) -> asyncio.Future:
739
720
 
740
- payload = {
721
+ async def mark_as_read(self, typing: bool = False) -> asyncio.Future:
722
+ """
723
+ Mark this inbound message as read.
724
+
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] = {
741
730
  "messaging_product": "whatsapp",
742
731
  "status": "read",
743
732
  "message_id": self.id,
744
733
  }
734
+ if typing:
735
+ payload["typing_indicator"] = {"type": "text"}
736
+
745
737
  async def call():
746
738
  async with aiohttp.ClientSession() as session:
747
- async with session.post(f"{self.instance.url}", headers=self.instance.headers, json=payload) as response:
739
+ async with session.post(
740
+ f"{self.instance.url}", headers=self.instance.headers, json=payload
741
+ ) as response:
748
742
  if response.status == 200:
749
- logging.info(await response.json())
750
- return await response.json()
751
- else:
752
- logging.error(await response.json())
743
+ logging.info("Request succeeded (status=%s)", response.status)
753
744
  return await response.json()
745
+ logging.error("Request failed (status=%s)", response.status)
746
+ return await response.json()
747
+
754
748
  f = asyncio.ensure_future(call())
755
- await asyncio.sleep(.001) # make asyncio run the task
749
+ await asyncio.sleep(0.001) # make asyncio run the task
756
750
  return f
757
-
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
+
758
756
  async def send(self, sender=None, preview_url: bool = True) -> asyncio.Future:
759
757
  try:
760
758
  sender = dict(self.instance.l)[sender]
761
759
 
762
- except:
760
+ except Exception:
763
761
  sender = self.instance.phone_number_id
764
762
 
765
- if sender == None:
763
+ if sender is None:
766
764
  sender = self.instance.phone_number_id
767
765
 
768
766
  url = f"https://graph.facebook.com/{self.instance.LATEST}/{sender}/messages"
@@ -774,21 +772,24 @@ class AsyncMessage(object):
774
772
  "text": {"preview_url": preview_url, "body": self.content},
775
773
  }
776
774
  logging.info(f"Sending message to {self.to}")
775
+
777
776
  async def call():
778
- print("sending")
779
777
  async with aiohttp.ClientSession() as session:
780
- async with session.post(url, headers=self.headers, json=data) as response:
778
+ async with session.post(
779
+ url, headers=self.headers, json=data
780
+ ) as response:
781
781
  if response.status == 200:
782
782
  logging.info(f"Message sent to {self.to}")
783
783
  return await response.json()
784
784
  logging.info(f"Message not sent to {self.to}")
785
785
  logging.info(f"Status code: {response.status}")
786
- logging.error(f"Response: {await response.json()}")
786
+ logging.error("Request failed (status=%s)", response.status)
787
787
  return await response.json()
788
+
788
789
  f = asyncio.ensure_future(call())
789
- await asyncio.sleep(.001) # make asyncio run the task
790
+ await asyncio.sleep(0.001) # make asyncio run the task
790
791
  return f
791
-
792
+
792
793
  async def react(self, emoji: str) -> asyncio.Future:
793
794
  data = {
794
795
  "messaging_product": "whatsapp",
@@ -798,16 +799,20 @@ class AsyncMessage(object):
798
799
  "reaction": {"message_id": self.id, "emoji": emoji},
799
800
  }
800
801
  logging.info(f"Reacting to {self.id}")
802
+
801
803
  async def call():
802
804
  async with aiohttp.ClientSession() as session:
803
- async with session.post(self.url, headers=self.headers, json=data) as response:
805
+ async with session.post(
806
+ self.url, headers=self.headers, json=data
807
+ ) as response:
804
808
  if response.status == 200:
805
809
  logging.info(f"Reaction sent to {self.to}")
806
810
  return await response.json()
807
811
  logging.info(f"Reaction not sent to {self.to}")
808
812
  logging.info(f"Status code: {response.status}")
809
- logging.debug(f"Response: {await response.json()}")
813
+ logging.debug("Request failed (status=%s)", response.status)
810
814
  return await response.json()
815
+
811
816
  f = asyncio.ensure_future(call())
812
- await asyncio.sleep(.001)
817
+ await asyncio.sleep(0.001)
813
818
  return f