whatsapp-python 4.0.2__py3-none-any.whl → 4.2.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
@@ -4,6 +4,9 @@ Unofficial Python wrapper for the WhatsApp Cloud API.
4
4
  from __future__ import annotations
5
5
  import requests
6
6
  import logging
7
+ import json
8
+ import asyncio
9
+ import aiohttp
7
10
  from bs4 import BeautifulSoup
8
11
  from fastapi import FastAPI, HTTPException, Request
9
12
  from uvicorn import run as _run
@@ -15,7 +18,14 @@ from .ext._send_media import send_image, send_video, send_audio, send_location,
15
18
  from .ext._media import upload_media, query_media_url, download_media, delete_media
16
19
  from .ext._buttons import send_button, create_button, send_reply_button
17
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
18
- import json
21
+
22
+ 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
24
+ 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
19
29
 
20
30
 
21
31
  class WhatsApp(object):
@@ -132,8 +142,8 @@ class WhatsApp(object):
132
142
  if r.query_params.get("hub.verify_token") == self.verify_token:
133
143
  logging.debug("Webhook verified successfully")
134
144
  challenge = r.query_params.get("hub.challenge")
135
- self.verification_handler(challenge)
136
- self.other_handler(challenge)
145
+ await self.verification_handler(challenge)
146
+ await self.other_handler(challenge)
137
147
  return int(challenge)
138
148
  logging.error("Webhook Verification failed - token mismatch")
139
149
  await self.verification_handler(False)
@@ -202,9 +212,9 @@ class WhatsApp(object):
202
212
  send_template = send_template
203
213
  send_custom_json = send_custom_json
204
214
  send_contacts = send_contacts
205
-
206
215
  authorized = property(authorized)
207
216
 
217
+
208
218
  def create_message(self, **kwargs) -> Message:
209
219
  """
210
220
  Create a message object
@@ -248,6 +258,238 @@ class WhatsApp(object):
248
258
  _run(self.app, host=host, port=port, **options)
249
259
 
250
260
 
261
+ 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)
428
+
429
+ send_button = async_send_button
430
+ create_button = async_create_button
431
+ send_reply_button = async_send_reply_button
432
+ send_image = async_send_image
433
+ send_video = async_send_video
434
+ send_audio = async_send_audio
435
+ send_location = async_send_location
436
+ send_sticker = async_send_sticker
437
+ send_document = async_send_document
438
+ upload_media = async_upload_media
439
+ query_media_url = async_query_media_url
440
+ download_media = async_download_media
441
+ delete_media = async_delete_media
442
+ send_template = async_send_template
443
+ send_custom_json = async_send_custom_json
444
+ send_contacts = async_send_contacts
445
+ authorized = property(async_authorized)
446
+
447
+
448
+
449
+ def create_message(self, **kwargs) -> AsyncMessage:
450
+ """
451
+ Create a message object
452
+
453
+ Args:
454
+ data[dict]: The message data
455
+ content[str]: The message content
456
+ to[str]: The recipient
457
+ rec_type[str]: The recipient type (individual/group)
458
+ """
459
+ return AsyncMessage(**kwargs, instance=self)
460
+
461
+ def on_message(self, handler: function):
462
+ """
463
+ Set the handler for incoming messages
464
+
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
+
251
493
  class Message(object):
252
494
  # type: ignore
253
495
  def __init__(self, id: int = None, data: dict = {}, instance: WhatsApp = None, content: str = "", to: str = "", rec_type: str = "individual"):
@@ -397,3 +639,175 @@ class Message(object):
397
639
  logging.info(f"Status code: {r.status_code}")
398
640
  logging.debug(f"Response: {r.json()}")
399
641
  return r.json()
642
+
643
+
644
+
645
+ class AsyncMessage(object):
646
+ # type: ignore
647
+ def __init__(self, id: int = None, data: dict = {}, instance: WhatsApp = None, content: str = "", to: str = "", rec_type: str = "individual"):
648
+ self.instance = instance
649
+ self.url = self.instance.url
650
+ self.headers = self.instance.headers
651
+
652
+ try:
653
+ self.id = instance.get_message_id(data)
654
+ except:
655
+ self.id = id
656
+ try:
657
+ self.type = self.instance.get_message_type(data)
658
+ except:
659
+ self.type = "text"
660
+ self.data = data
661
+ self.rec = rec_type
662
+ self.to = to
663
+ try:
664
+ self.content = content if content != "" else self.instance.get_message(
665
+ data)
666
+ except:
667
+ self.content = content
668
+ try:
669
+ self.name = self.instance.get_name(data)
670
+ except:
671
+ self.name = None
672
+
673
+ if self.type == "image":
674
+ try:
675
+ self.image = self.instance.get_image(data)
676
+ except:
677
+ self.image = None
678
+ if self.type == "sticker":
679
+ try:
680
+ self.sticker = self.instance.get_sticker(data)
681
+ except:
682
+ self.sticker = None
683
+ elif self.type == "video":
684
+ try:
685
+ self.video = self.instance.get_video(data)
686
+ except:
687
+ self.video = None
688
+ elif self.type == "audio":
689
+ try:
690
+ self.audio = self.instance.get_audio(data)
691
+ except:
692
+ pass
693
+ elif self.type == "document":
694
+ try:
695
+ self.document = self.instance.get_document(data)
696
+ except:
697
+ pass
698
+ elif self.type == "location":
699
+ try:
700
+ self.location = self.instance.get_location(data)
701
+ except:
702
+ pass
703
+ elif self.type == "interactive":
704
+ try:
705
+ self.interactive = self.instance.get_interactive_response(data)
706
+ except:
707
+ pass
708
+
709
+
710
+ async def reply(self, reply_text: str = "", preview_url: bool = True) -> asyncio.Future:
711
+ if self.data == {}:
712
+ return {"error": "No data provided"}
713
+ author = self.instance.get_author(self.data)
714
+ payload = {
715
+ "messaging_product": "whatsapp",
716
+ "recipient_type": "individual",
717
+ "to": str(author),
718
+ "type": "text",
719
+ "context": {"message_id": self.id},
720
+ "text": {"preview_url": preview_url, "body": reply_text},
721
+ }
722
+ logging.info(f"Replying to {self.id}")
723
+ async def call():
724
+ async with aiohttp.ClientSession() as session:
725
+ async with session.post(self.url, headers=self.headers, json=payload) as response:
726
+ if response.status == 200:
727
+ logging.info(f"Message sent to {self.instance.get_author(self.data)}")
728
+ return await response.json()
729
+ logging.info(f"Message not sent to {self.instance.get_author(self.data)}")
730
+ logging.info(f"Status code: {response.status}")
731
+ logging.error(f"Response: {await response.json()}")
732
+ return await response.json()
733
+ f = asyncio.ensure_future(call())
734
+ await asyncio.sleep(.001) # make asyncio run the task
735
+ return f
736
+
737
+
738
+ async def mark_as_read(self) -> asyncio.Future:
739
+
740
+ payload = {
741
+ "messaging_product": "whatsapp",
742
+ "status": "read",
743
+ "message_id": self.id,
744
+ }
745
+ async def call():
746
+ async with aiohttp.ClientSession() as session:
747
+ async with session.post(f"{self.instance.url}", headers=self.instance.headers, json=payload) as response:
748
+ if response.status == 200:
749
+ logging.info(await response.json())
750
+ return await response.json()
751
+ else:
752
+ logging.error(await response.json())
753
+ return await response.json()
754
+ f = asyncio.ensure_future(call())
755
+ await asyncio.sleep(.001) # make asyncio run the task
756
+ return f
757
+
758
+ async def send(self, sender=None, preview_url: bool = True) -> asyncio.Future:
759
+ try:
760
+ sender = dict(self.instance.l)[sender]
761
+
762
+ except:
763
+ sender = self.instance.phone_number_id
764
+
765
+ if sender == None:
766
+ sender = self.instance.phone_number_id
767
+
768
+ url = f"https://graph.facebook.com/{self.instance.LATEST}/{sender}/messages"
769
+ data = {
770
+ "messaging_product": "whatsapp",
771
+ "recipient_type": self.rec,
772
+ "to": self.to,
773
+ "type": "text",
774
+ "text": {"preview_url": preview_url, "body": self.content},
775
+ }
776
+ logging.info(f"Sending message to {self.to}")
777
+ async def call():
778
+ print("sending")
779
+ async with aiohttp.ClientSession() as session:
780
+ async with session.post(url, headers=self.headers, json=data) as response:
781
+ if response.status == 200:
782
+ logging.info(f"Message sent to {self.to}")
783
+ return await response.json()
784
+ logging.info(f"Message not sent to {self.to}")
785
+ logging.info(f"Status code: {response.status}")
786
+ logging.error(f"Response: {await response.json()}")
787
+ return await response.json()
788
+ f = asyncio.ensure_future(call())
789
+ await asyncio.sleep(.001) # make asyncio run the task
790
+ return f
791
+
792
+ async def react(self, emoji: str) -> asyncio.Future:
793
+ data = {
794
+ "messaging_product": "whatsapp",
795
+ "recipient_type": "individual",
796
+ "to": self.to,
797
+ "type": "reaction",
798
+ "reaction": {"message_id": self.id, "emoji": emoji},
799
+ }
800
+ logging.info(f"Reacting to {self.id}")
801
+ async def call():
802
+ async with aiohttp.ClientSession() as session:
803
+ async with session.post(self.url, headers=self.headers, json=data) as response:
804
+ if response.status == 200:
805
+ logging.info(f"Reaction sent to {self.to}")
806
+ return await response.json()
807
+ logging.info(f"Reaction not sent to {self.to}")
808
+ logging.info(f"Status code: {response.status}")
809
+ logging.debug(f"Response: {await response.json()}")
810
+ return await response.json()
811
+ f = asyncio.ensure_future(call())
812
+ await asyncio.sleep(.001)
813
+ return f
@@ -0,0 +1,118 @@
1
+ import logging
2
+ import aiohttp
3
+ import asyncio
4
+ from typing import Dict, Any
5
+
6
+
7
+ def create_button(self, button: Dict[Any, Any]) -> Dict[Any, Any]:
8
+ """
9
+ Method to create a button object to be used in the send_message method.
10
+
11
+ This is method is designed to only be used internally by the send_button method.
12
+
13
+ Args:
14
+ button[dict]: A dictionary containing the button data
15
+ """
16
+ data = {"type": "list", "action": button.get("action")}
17
+ if button.get("header"):
18
+ data["header"] = {"type": "text", "text": button.get("header")}
19
+ if button.get("body"):
20
+ data["body"] = {"text": button.get("body")}
21
+ if button.get("footer"):
22
+ data["footer"] = {"text": button.get("footer")}
23
+ if button.get("type"):
24
+ data["type"] = button.get("type")
25
+ return data
26
+
27
+
28
+ async def send_button(self, button: Dict[Any, Any], recipient_id: str, sender=None) -> asyncio.Future:
29
+ """
30
+ Sends an interactive buttons message to a WhatsApp user
31
+
32
+ Args:
33
+ button[dict]: A dictionary containing the button data(rows-title may not exceed 20 characters)
34
+ recipient_id[str]: Phone number of the user with country code wihout +
35
+
36
+ check https://github.com/Neurotech-HQ/whatsapp#sending-interactive-reply-buttons for an example.
37
+ """
38
+ try:
39
+ sender = dict(self.l)[sender]
40
+
41
+ except:
42
+ sender = self.phone_number_id
43
+
44
+ if sender == None:
45
+ sender = self.phone_number_id
46
+
47
+ url = f"https://graph.facebook.com/{self.LATEST}/{sender}/messages"
48
+ data = {
49
+ "messaging_product": "whatsapp",
50
+ "to": recipient_id,
51
+ "type": "interactive",
52
+ "interactive": self.create_button(button),
53
+ }
54
+ logging.info(f"Sending buttons to {recipient_id}")
55
+ async def call():
56
+ async with aiohttp.ClientSession() as session:
57
+ async with session.post(url, headers=self.headers, json=data) as r:
58
+ if r.status == 200:
59
+ logging.info(f"Buttons sent to {recipient_id}")
60
+ return await r.json()
61
+ logging.info(f"Buttons not sent to {recipient_id}")
62
+ logging.info(f"Status code: {r.status}")
63
+ logging.info(f"Response: {await r.json()}")
64
+ return await r.json()
65
+ f = asyncio.ensure_future(call())
66
+ await asyncio.sleep(.001) # make asyncio run the task
67
+ return f
68
+
69
+
70
+
71
+ async def send_reply_button(
72
+ self, button: Dict[Any, Any], recipient_id: str, sender=None
73
+ ) -> asyncio.Future:
74
+ """
75
+ Sends an interactive reply buttons[menu] message to a WhatsApp user
76
+
77
+ Args:
78
+ button[dict]: A dictionary containing the button data
79
+ recipient_id[str]: Phone number of the user with country code wihout +
80
+
81
+ Note:
82
+ The maximum number of buttons is 3, more than 3 buttons will rise an error.
83
+ """
84
+ try:
85
+ sender = dict(self.l)[sender]
86
+
87
+ except:
88
+ sender = self.phone_number_id
89
+
90
+ if sender == None:
91
+ sender = self.phone_number_id
92
+
93
+ url = f"https://graph.facebook.com/{self.LATEST}/{sender}/messages"
94
+ if len(button["action"]["buttons"]) > 3:
95
+ raise ValueError("The maximum number of buttons is 3.")
96
+
97
+ data = {
98
+ "messaging_product": "whatsapp",
99
+ "recipient_type": "individual",
100
+ "to": recipient_id,
101
+ "type": "interactive",
102
+ "interactive": button,
103
+ }
104
+ logging.info(f"Sending buttons to {recipient_id}")
105
+ async def call():
106
+ async with aiohttp.ClientSession() as session:
107
+ async with session.post(url, headers=self.headers, json=data) as r:
108
+ if r.status == 200:
109
+ logging.info(f"Buttons sent to {recipient_id}")
110
+ return await r.json()
111
+ logging.info(f"Buttons not sent to {recipient_id}")
112
+ logging.info(f"Status code: {r.status}")
113
+ logging.info(f"Response: {await r.json()}")
114
+ return await r.json()
115
+
116
+ f = asyncio.ensure_future(call())
117
+ await asyncio.sleep(.001) # make asyncio run the task
118
+ return f