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 CHANGED
@@ -3,15 +3,19 @@ Unofficial Python wrapper for the WhatsApp Cloud API.
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
- import requests
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
- from bs4 import BeautifulSoup
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 send_button, create_button, send_reply_button
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
- from .errors import Handle
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[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
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 # package version
139
- r = requests.get(
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
- latest = str(
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
- data_str = json.dumps(data, indent=4)
244
- # log the data received only if the log level is debug
245
- 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")
246
257
 
247
- changed_field = self.changed_field(data)
248
- if changed_field == "messages":
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 = Message(instance=self, data=data)
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: function):
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: function):
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: function):
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
- def __init__(
344
- self,
345
- token: str = "",
346
- phone_number_id: dict = "",
347
- logger: bool = True,
348
- update_check: bool = True,
349
- verify_token: str = "",
350
- debug: bool = True,
351
- version: str = "latest",
352
- ):
353
- """
354
- Initialize the WhatsApp Object
355
-
356
- Args:
357
- token[str]: Token for the WhatsApp cloud API obtained from the Facebook developer portal
358
- phone_number_id[str]: Phone number id for the WhatsApp cloud API obtained from the developer portal
359
- logger[bool]: Whether to enable logging or not (default: True)
360
- """
361
-
362
- # Check if the version is up to date
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 a message object
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: int = None,
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
- 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
+
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(f"Response: {r.json()}")
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
- 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] = {
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.json())
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 == None:
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(f"Response: {r.json()}")
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(f"Response: {r.json()}")
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: int = None,
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
- self.to = to
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(f"Response: {await response.json()}")
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
- payload = {
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(await response.json())
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 == None:
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(f"Response: {await response.json()}")
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(f"Response: {await response.json()}")
813
+ logging.debug("Request failed (status=%s)", response.status)
940
814
  return await response.json()
941
815
 
942
816
  f = asyncio.ensure_future(call())