From e71d1d64e43cf5d6827fd9791a1441af3a148921 Mon Sep 17 00:00:00 2001 From: Jorge Juarez Date: Tue, 18 Jun 2024 08:46:18 -0600 Subject: [PATCH 01/22] =?UTF-8?q?=F0=9F=94=A5=20remove=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy_brach.yml | 48 ------------------------------ hook.py | 39 ------------------------ 2 files changed, 87 deletions(-) delete mode 100644 .github/workflows/deploy_brach.yml delete mode 100644 hook.py diff --git a/.github/workflows/deploy_brach.yml b/.github/workflows/deploy_brach.yml deleted file mode 100644 index 632387f..0000000 --- a/.github/workflows/deploy_brach.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Copy files to deploy branch - -on: - push: - branches: - - main # Rama principal - -jobs: - sync: - runs-on: ubuntu-latest - - steps: - - name: Checkout main branch - uses: actions/checkout@v2 - with: - fetch-depth: 0 - ref: main - - - name: Copy files - run: | - mkdir -p deployment_directory/pynani - cp -r pynani deployment_directory/pynani - cp pyproject.toml deployment_directory/ - cp README.md deployment_directory/ - cp LICENSE deployment_directory/ - cp .gitignore deployment_directory/ - - - name: Checkout deployment branch - run: | - git checkout deployment-branch - - - name: Configure Git - run: | - git config --global user.name "jorge-jrzz" - git config --global user.email "jorgeang33@gmail.com" - - - name: Copy to Deploy Branch - run: | - cp -r deployment_directory/* . - rm -rf deployment_directory - git add . - - - name: Commit and push changes - env: - ACTIONS_DEPLOY_TOKEN: ${{ secrets.ACTIONS_DEPLOY_TOKEN }} - run: | - git commit -m "馃殌 Deploy files" || echo "No changes to commit" - git push https://x-access-token:${{ secrets.ACTIONS_DEPLOY_TOKEN }}@github.com/jorge-jrzz/Pynani.git deployment-branch diff --git a/hook.py b/hook.py deleted file mode 100644 index f5632c4..0000000 --- a/hook.py +++ /dev/null @@ -1,39 +0,0 @@ -from flask import Flask, request, jsonify -from pynani import Messenger, Buttons - -PAGE_ACCESS_TOKEN = 'EAAL4nnkuqAcBO7WJZBbXWineRCJaQv3yvPL3DfzQyrmV6PiUNYrTxxHuZC7S3bWC2fnzGwaxNR5uPJ4OhuEOKpvBensrlHPId6pXHdOZBw4J70RC1jgRhhp9137TgoGRSlpZAdZCHAeBwmloZBqGWNmLUv9CPkHYUY6iIqx23SuOQu1sdghVOnmjSDr0qBc9ol' -app = Flask(__name__) -mess = Messenger(PAGE_ACCESS_TOKEN) -b = Buttons() - -@app.route("/", methods=['GET']) -def meta_verify(): - return mess.verify_token(request.args, '12345') - -@app.route("/", methods=['POST']) -def meta_webhook(): - # print("Algo paso!!!") - data = request.get_json() - mensaje = mess.get_message_text(data) - # print(f'\nData: {data}\n') - # print(mensaje) - # print(mess.get_message_type(data)) - sender_id = mess.get_sender_id(data) - if mensaje == "hola": - mess.send_text_message(sender_id, ["Hola", "驴C贸mo est谩s?"]) - if mensaje == "botones simples": - botones = b.basic_buttons(["Opci贸n 1", "Opci贸n 2", "馃敟"]) - mess.send_button_template(sender_id, "Hola, 驴qu茅 deseas hacer?", botones) - elif mensaje == "botones complicados": - cb = b.leave_buttons([{'title': 'Opci贸n 1', 'url': 'https://www.google.com'}, {'title': 'Opci贸n 2', 'call_number': '+525555555555'}]) - mess.send_button_template(sender_id, "Hola, 驴qu茅 deseas hacer?", cb) - elif mensaje == "imagen": - r = mess.send_local_attachment(sender_id, 'image', './test.png') - print(f'\n\n{r}') - elif mensaje == "ata": - r = mess.upload_attachment('image', './test.png') - print(f'\n\n{r}') - return jsonify({"status": "success"}), 200 - -if __name__ =='__main__': - app.run(port=8080, debug=True) \ No newline at end of file From b35f48913a4eb38adbeab5337fe117760a00da6b Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Wed, 19 Jun 2024 15:05:20 +0000 Subject: [PATCH 02/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pynani/Messenger.py | 119 ++++++++++++++++++++++++++------------- Pynani/__init__.py | 3 +- Pynani/utils/elements.py | 44 +++++++++++++++ 3 files changed, 126 insertions(+), 40 deletions(-) create mode 100644 Pynani/utils/elements.py diff --git a/Pynani/Messenger.py b/Pynani/Messenger.py index 4436480..ac9cbb0 100644 --- a/Pynani/Messenger.py +++ b/Pynani/Messenger.py @@ -27,7 +27,7 @@ def get_sender_id(self, data: dict): except (IndexError, KeyError) as e: print(f"Error accessing sender ID: {e}") return None - + def get_message_type(self, data: dict): messaging = data['entry'][0]['messaging'][0] try: @@ -50,7 +50,7 @@ def get_message_type(self, data: dict): except (IndexError, KeyError) as e: print(f"Error accessing message type: {e}") return None - + def get_message_text(self, data: dict): try: message = data['entry'][0]['messaging'][0] @@ -75,17 +75,19 @@ def send_text_message(self, sender_id, message: Union[str, int]): "text": message } } - - try: - r = requests.post(self._url, headers=header, json=payload, timeout=10) + + try: + r = requests.post(self._url, headers=header, + json=payload, timeout=10) r.raise_for_status() return r.json() except requests.exceptions.RequestException as e: print(f"Request failed: {e} \n{r.json()}") return None - + def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: - attachments_url = f"https://graph.facebook.com/v20.0/{self.page_id}/message_attachments" + attachments_url = f"https://graph.facebook.com/v20.0/{ + self.page_id}/message_attachments" attachment = Path(attachment_path) mimetype, _ = mimetypes.guess_type(attachment) @@ -101,68 +103,69 @@ def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: } } file = { - "filedata": (attachment.name, attachment.open('rb'), mimetype) - } - body = {"message": str(message)} + "filedata": (attachment.name, attachment.open('rb'), mimetype) + } + body = {"message": str(message)} + + r = requests.post(attachments_url, headers=header, + files=file, data=body, timeout=20) - r = requests.post(attachments_url, headers=header, files=file, data=body, timeout=20) - - try : + try: attachment_id = r.json()["attachment_id"] return attachment_id except KeyError as e: print(f"Error uploading attachment: {e}") return None - + def get_url_attachment(self, data: dict): try: return data['entry'][0]['messaging'][0]['message']['attachments'][0]["payload"]["url"] except (IndexError, KeyError) as e: print(f"Error accessing attachment url: {e}") return None - + def get_attachment_type(self, data: dict): try: return data['entry'][0]['messaging'][0]['message']['attachments'][0]["type"] except (IndexError, KeyError) as e: print(f"Error accessing attachment type: {e}") return None - + def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: str): header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} payload = { - "recipient":{ + "recipient": { "id": sender_id }, "messaging_type": "RESPONSE", "message": { "attachment": { - "type": attachment_type, - "payload": { - "url": attachment_url, - "is_reusable": True - } + "type": attachment_type, + "payload": { + "url": attachment_url, + "is_reusable": True + } } } } - + r = requests.post(self._url, headers=header, json=payload, timeout=10) return r.json() - + def send_local_attachment(self, sender_id: str, attachment_type: str, attachment_path: str): attachment = Path(attachment_path) - mimetype, _ = mimetypes.guess_type(attachment) + mimetype, _ = mimetypes.guess_type(attachment) recipient = {"id": sender_id} message = { - "attachment": { - "type": attachment_type, - "payload": { - "is_reusable": "true" - } + "attachment": { + "type": attachment_type, + "payload": { + "is_reusable": "true" } } - + } + header = {"Authorization": f"Bearer {self.access_token}"} body = { "recipient": str(recipient), @@ -172,9 +175,10 @@ def send_local_attachment(self, sender_id: str, attachment_type: str, attachment "filedata": (attachment.name, attachment.open('rb'), mimetype) } - r = requests.post(self._url, headers=header, data=body, files=file, timeout=10) + r = requests.post(self._url, headers=header, + data=body, files=file, timeout=10) return r.json() - + def download_attachment(self, attachment_url: str, path_dest: str): response = requests.get(attachment_url, stream=True, timeout=10) if response.status_code == 200: @@ -185,7 +189,7 @@ def download_attachment(self, attachment_url: str, path_dest: str): print('Downloaded attachment successfully!') else: print('Error downloading attachment') - + def send_quick_reply(self, sender_id, message: str, quick_replies: list): if len(quick_replies) > 13: print("Quick replies should be less than 13") @@ -205,7 +209,7 @@ def send_quick_reply(self, sender_id, message: str, quick_replies: list): r = requests.post(self._url, headers=header, json=payload, timeout=10) return r.json() - + def send_button_template(self, sender_id: str, message: str, buttons: list): header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} @@ -228,7 +232,7 @@ def send_button_template(self, sender_id: str, message: str, buttons: list): r = requests.post(self._url, headers=header, json=payload, timeout=10) return r.json() - + def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: list): header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} @@ -256,7 +260,7 @@ def send_media_template(self, sender_id: str, media_type: str, attachment_id: st r = requests.post(self._url, headers=header, json=body, timeout=10) return r.json() - + def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, subtitle: Optional[str] = None, buttons: Optional[list] = None): if default_url: default_action = { @@ -269,7 +273,7 @@ def send_generic_template(self, sender_id: str, title: str, image_url: Optional[ default_action = None header = {"Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}"} + "Authorization": f"Bearer {self.access_token}"} body = { "recipient": { "id": sender_id @@ -292,7 +296,44 @@ def send_generic_template(self, sender_id: str, title: str, image_url: Optional[ } } } - + + try: + r = requests.post(self._url, headers=header, json=body, timeout=10) + r.raise_for_status() + return r.json() + except requests.exceptions.RequestException as e: + print(f"Request failed: {e} \n{r.json()}") + return None + + def send_receipt_template(self, sender_id: str, order_number: str, payment_method: str, summary: dict, currency: str = 'USD', + order_url: Optional[str] = None, timestamp: Optional[str] = None, address: Optional[dict] = None, + adjustments: Optional[list] = None, elements: Optional[list] = None): + header = {"Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}"} + body = { + "recipient": { + "id": sender_id + }, + "message": { + "attachment": { + "type": "template", + "payload": { + "template_type": "receipt", + "recipient_name": "Stephane Crozatier", + "order_number": order_number, + "currency": currency, + "payment_method": payment_method, + "order_url": order_url, + "timestamp": timestamp, + "address": address, + "summary": summary, + "adjustments": adjustments, + "elements": elements + } + } + } + } + try: r = requests.post(self._url, headers=header, json=body, timeout=10) r.raise_for_status() diff --git a/Pynani/__init__.py b/Pynani/__init__.py index 9ba6f29..5c267d1 100644 --- a/Pynani/__init__.py +++ b/Pynani/__init__.py @@ -1,3 +1,4 @@ from .Messenger import Messenger from .utils.QuickReply import QuickReply -from .utils.Buttons import Buttons \ No newline at end of file +from .utils.Buttons import Buttons +from .utils import elements diff --git a/Pynani/utils/elements.py b/Pynani/utils/elements.py new file mode 100644 index 0000000..6442746 --- /dev/null +++ b/Pynani/utils/elements.py @@ -0,0 +1,44 @@ +from typing import Optional, List, Any + + +def get_address(street_1: str, city: str, postal_code: str, state: str, country: str, street_2: Optional[str] = None) -> dict: + return { + "street_1": street_1, + "street_2": street_2, + "city": city, + "postal_code": postal_code, + "state": state, + "country": country + } + + +def get_summary(total_cost: float, subtotal: Optional[float] = None, shipping_cost: Optional[float] = None, total_tax: Optional[float] = None): + return { + "subtotal": subtotal, + "shipping_cost": shipping_cost, + "total_tax": total_tax, + "total_cost": total_cost + } + + +def get_adjustments(*args: Any) -> list: + adjustments = [] + for i in range(0, len(args), 2): + adjustment = { + "name": args[i], + "amount": args[i + 1] + } + adjustments.append(adjustment) + return adjustments + + +def get_elements(title: str, price: float, subtitle: Optional[str] = None, quantity: Optional[int] = None, currency: Optional[str] = 'USD', image_url: Optional[str] = None) -> List[dict]: + item = { + "title": title, + "subtitle": subtitle, + "quantity": quantity, + "price": price, + "currency": currency, + "image_url": image_url + } + return [item] From 6e351e765a6dc6b64a62949dffaebfbd8beb892a Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Thu, 20 Jun 2024 20:51:14 +0000 Subject: [PATCH 03/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d5dc5d4..3f3b53b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,43 @@ [build-system] -requires = ["setuptools>=42", "wheel"] build-backend = "setuptools.build_meta" +requires = ["setuptools >= 61.0", "wheel"] [project] -name = "pynani" -version = "1.1.0" -description = "A package to wrap the Messenger API" -readme = "README.md" authors = [ - { name = "Jorge Juarez", email = "jorgeang33@gmail.com" }, + {name = "Jorge Juarez", email = "jorgeang33@gmail.com"}, ] -license = { text = "MIT" } classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Customer Service", + "Topic :: Software Development :: Build Tools", + "Topic :: Communications :: Chat", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Version Control :: Git", + "Topic :: Utilities", + "License :: OSI Approved :: MIT License", # "Topic :: Documentation :: Sphinx" + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ - "requests>=2.32.3", + "requests>=2.32.3", ] +description = "A package to wrap the Messenger API" +keywords = ["Messenger", "wrapper", "Meta", "API", "Facebook", "Instagram"] +license = {file = "LICENSE"} +name = "pynani" +readme = "README.md" +requires-python = ">=3.8" +version = "1.2.0" + [project.urls] -"Homepage" = "https://github.com/jorge-jrzz/Pynani/tree/main" \ No newline at end of file +"Bug Tracker" = "https://github.com/jorge-jrzz/Pynani/issues" +Repository = "https://github.com/jorge-jrzz/Pynani" From 3207cfade83e413480ec13b70bbe4a933c2b2c15 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Sat, 22 Jun 2024 06:42:27 +0000 Subject: [PATCH 04/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5059123..197c16e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,141 @@ -# Pynani -Opensource python wrapper to Messenger and Instagram API +banner Pynani + +--- + +Opensource python wrapper to Messenger API + +## Features + +### API + +- Verify the webhook +- Send text messages +- Send attachments from a remote file (image, audio, video, file) +- Send attachments from a local file (image, audio, video, file) +- Send templates (generic, buttons, media, receipent) +- Send quick replies + +### Other functionalities + +- Get sender id +- Get type of message received +- Get text of the message received +- Get the url of the attachment received +- Get type of the attachment received +- Download attachments received + +## Installation + +Install Pynani with pip + +```bash + pip install pynani +``` + +Or install with pipenv (requires pipenv installed) + +```bash + pipenv install pynani +``` + +## Getting started + +### Prerequisites + +- **Python 3.8**+ installed +- To get started using this module, you will need **page access token** which you can get from the Facebook Developer Portal + +### A simple echo bot + +The Messenger class (defined in Messenger.py) encapsulates all API calls in a single class. It provides functions such as send_xyz (send_message, send_attachment etc.) and several ways to listen for incoming messages. + +Create a file called echo_bot.py. Then, open the file and create an instance of the Messenger class. + +```python +from pynani import Messenger + +PAGE_ACCESS_TOKEN = 'EAAxxxxxxx...' +mess = Messenger(PAGE_ACCESS_TOKEN) +``` + +> [!IMPORTANT] +> Make sure to actually replace PAGE_ACCESS_TOKEN with your own page access token. + +After that declaration, we need to register some message handlers. First, we need to create and verify a webhook with the help of _Flask_ or _FastAPI_. + +```python +from flask import Flask, request, jsonify + +app = Flask(__name__) + +TOKEN = "abc123" + +@app.get("/") +def meta_verify(): + return mess.verify_token(request.args, TOKEN) +``` + +Now let's define a webhook that handles certain messages + +```python +@app.post("/") +def meta_webhook(): + data = request.get_json() + sender_id = mess.get_sender_id(data) + message = mess.get_message_text(data) + if mensaje == "hello": + mess.send_text_message(sender_id, "Hello, World!") + if mensaje == "bye": + mess.send_text_message(sender_id, "Nice to meet you! 馃憤馃徑") + + return jsonify({"status": "success"}), 200 +``` + +We now have a basic bot which replies a static message to "hello" and "bye" messages. To start the bot, add the following to our source file: + +```python +if __name__ =='__main__': + app.run(port=8080, debug=True) +``` + +Alright, that's it! Our source file now looks like this: + +```python +from flask import Flask, request, jsonify +from pynani import Messenger + +PAGE_ACCESS_TOKEN = 'EAAxxxxxxx...' +TOKEN = "abc123" + +mess = Messenger(PAGE_ACCESS_TOKEN) +app = Flask(__name__) + +@app.get("/") +def meta_verify(): + return mess.verify_token(request.args, TOKEN) + +@app.post("/") +def meta_webhook(): + data = request.get_json() + sender_id = mess.get_sender_id(data) + message = mess.get_message_text(data) + if mensaje == "hello": + mess.send_text_message(sender_id, "Hello, World!") + if mensaje == "bye": + mess.send_text_message(sender_id, "Nice to meet you! 馃憤馃徑") + + return jsonify({"status": "success"}), 200 + +if __name__ =='__main__': + app.run(port=8080, debug=True) +``` + +To start the bot, simply open up a terminal and enter `python echo_bot.py` to run the bot! Test it by sending messages ("hello" and "bye"). + +## Related + +Here are some related projects that I was inspired by them. + +- [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI?tab=readme-ov-file) +- [WhatsApp Cloud API Wrapper](https://github.com/Neurotech-HQ/heyoo) +- [Messenger API Python](https://github.com/krishna2206/messenger-api-python) From ebc63ca4acd1d1dd8ed8afc829a1fa1089df2147 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Sun, 23 Jun 2024 02:09:25 +0000 Subject: [PATCH 05/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +- pynani/Messenger.py | 343 +++++++++++++++++++++++++++++++++++++ pynani/__init__.py | 4 + pynani/utils/Buttons.py | 52 ++++++ pynani/utils/QuickReply.py | 42 +++++ pynani/utils/elements.py | 44 +++++ 6 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 pynani/Messenger.py create mode 100644 pynani/__init__.py create mode 100644 pynani/utils/Buttons.py create mode 100644 pynani/utils/QuickReply.py create mode 100644 pynani/utils/elements.py diff --git a/README.md b/README.md index 197c16e..6738403 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,9 @@ def meta_webhook(): data = request.get_json() sender_id = mess.get_sender_id(data) message = mess.get_message_text(data) - if mensaje == "hello": + if message == "Hello": mess.send_text_message(sender_id, "Hello, World!") - if mensaje == "bye": + if message == "Bye": mess.send_text_message(sender_id, "Nice to meet you! 馃憤馃徑") return jsonify({"status": "success"}), 200 @@ -119,9 +119,9 @@ def meta_webhook(): data = request.get_json() sender_id = mess.get_sender_id(data) message = mess.get_message_text(data) - if mensaje == "hello": + if message == "Hello": mess.send_text_message(sender_id, "Hello, World!") - if mensaje == "bye": + if message == "Bye": mess.send_text_message(sender_id, "Nice to meet you! 馃憤馃徑") return jsonify({"status": "success"}), 200 diff --git a/pynani/Messenger.py b/pynani/Messenger.py new file mode 100644 index 0000000..ac9cbb0 --- /dev/null +++ b/pynani/Messenger.py @@ -0,0 +1,343 @@ +from typing import Union, Optional +from pathlib import Path +import mimetypes +import requests + + +class Messenger(): + def __init__(self, access_token: str, page_id: str = 'me'): + self.access_token = access_token + self.page_id = page_id + self._url = f"https://graph.facebook.com/v20.0/{page_id}/messages" + + def verify_token(self, params, token): + mode = params.get("hub.mode") + hub_token = params.get("hub.verify_token") + challenge = params.get("hub.challenge") + + if mode == "subscribe" and challenge: + if hub_token != token: + return "Verification token mismatch", 403 + return challenge, 200 + return "Hello world", 200 + + def get_sender_id(self, data: dict): + try: + return data['entry'][0]['messaging'][0]['sender']['id'] + except (IndexError, KeyError) as e: + print(f"Error accessing sender ID: {e}") + return None + + def get_message_type(self, data: dict): + messaging = data['entry'][0]['messaging'][0] + try: + if 'postback' in messaging: + return 'postback' + message_type = messaging['message'] + if 'text' in message_type: + if 'attachments' in message_type: + if message_type['attachments'][0]['type'] == 'fallback': + return 'link' + return 'text' + if 'attachments' in message_type: + attachment_type = message_type['attachments'][0]['type'] + if 'image' in attachment_type: + if 'sticker_id' in message_type['attachments'][0]['payload']: + return 'sticker' + return 'image' + else: + return attachment_type + except (IndexError, KeyError) as e: + print(f"Error accessing message type: {e}") + return None + + def get_message_text(self, data: dict): + try: + message = data['entry'][0]['messaging'][0] + if 'message' in message: + # print("Hola?") + return message['message']['text'] + elif 'postback' in message: + return message['postback']['title'] + except (IndexError, KeyError) as e: + print(f"Error accessing message text: {e}") + return None + + def send_text_message(self, sender_id, message: Union[str, int]): + header = {"Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}"} + payload = { + "recipient": { + "id": sender_id + }, + "messaging_type": "RESPONSE", + "message": { + "text": message + } + } + + try: + r = requests.post(self._url, headers=header, + json=payload, timeout=10) + r.raise_for_status() + return r.json() + except requests.exceptions.RequestException as e: + print(f"Request failed: {e} \n{r.json()}") + return None + + def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: + attachments_url = f"https://graph.facebook.com/v20.0/{ + self.page_id}/message_attachments" + attachment = Path(attachment_path) + mimetype, _ = mimetypes.guess_type(attachment) + + header = { + "Authorization": f"Bearer {self.access_token}" + } + message = { + "attachment": { + "type": attachment_type, + "payload": { + "is_reusable": "true" + } + } + } + file = { + "filedata": (attachment.name, attachment.open('rb'), mimetype) + } + body = {"message": str(message)} + + r = requests.post(attachments_url, headers=header, + files=file, data=body, timeout=20) + + try: + attachment_id = r.json()["attachment_id"] + return attachment_id + except KeyError as e: + print(f"Error uploading attachment: {e}") + return None + + def get_url_attachment(self, data: dict): + try: + return data['entry'][0]['messaging'][0]['message']['attachments'][0]["payload"]["url"] + except (IndexError, KeyError) as e: + print(f"Error accessing attachment url: {e}") + return None + + def get_attachment_type(self, data: dict): + try: + return data['entry'][0]['messaging'][0]['message']['attachments'][0]["type"] + except (IndexError, KeyError) as e: + print(f"Error accessing attachment type: {e}") + return None + + def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: str): + header = {"Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}"} + payload = { + "recipient": { + "id": sender_id + }, + "messaging_type": "RESPONSE", + "message": { + "attachment": { + "type": attachment_type, + "payload": { + "url": attachment_url, + "is_reusable": True + } + } + } + } + + r = requests.post(self._url, headers=header, json=payload, timeout=10) + return r.json() + + def send_local_attachment(self, sender_id: str, attachment_type: str, attachment_path: str): + attachment = Path(attachment_path) + mimetype, _ = mimetypes.guess_type(attachment) + recipient = {"id": sender_id} + message = { + "attachment": { + "type": attachment_type, + "payload": { + "is_reusable": "true" + } + } + } + + header = {"Authorization": f"Bearer {self.access_token}"} + body = { + "recipient": str(recipient), + "message": str(message) + } + file = { + "filedata": (attachment.name, attachment.open('rb'), mimetype) + } + + r = requests.post(self._url, headers=header, + data=body, files=file, timeout=10) + return r.json() + + def download_attachment(self, attachment_url: str, path_dest: str): + response = requests.get(attachment_url, stream=True, timeout=10) + if response.status_code == 200: + with open(path_dest, 'wb') as file: + for chunk in response.iter_content(1024): + file.write(chunk) + + print('Downloaded attachment successfully!') + else: + print('Error downloading attachment') + + def send_quick_reply(self, sender_id, message: str, quick_replies: list): + if len(quick_replies) > 13: + print("Quick replies should be less than 13") + quick_replies = quick_replies[:13] + header = {"Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}"} + payload = { + "recipient": { + "id": sender_id + }, + "messaging_type": "RESPONSE", + "message": { + "text": message, + "quick_replies": quick_replies + } + } + + r = requests.post(self._url, headers=header, json=payload, timeout=10) + return r.json() + + def send_button_template(self, sender_id: str, message: str, buttons: list): + header = {"Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}"} + payload = { + "recipient": { + "id": sender_id + }, + "messaging_type": "RESPONSE", + "message": { + "attachment": { + "type": "template", + "payload": { + "template_type": "button", + "text": message, + "buttons": buttons + } + } + } + } + + r = requests.post(self._url, headers=header, json=payload, timeout=10) + return r.json() + + def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: list): + header = {"Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}"} + body = { + "recipient": { + "id": sender_id + }, + "messaging_type": "RESPONSE", + "message": { + "attachment": { + "type": "template", + "payload": { + "template_type": "media", + "elements": [ + { + "media_type": media_type, + "attachment_id": attachment_id, + "buttons": buttons + } + ] + } + } + } + } + + r = requests.post(self._url, headers=header, json=body, timeout=10) + return r.json() + + def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, subtitle: Optional[str] = None, buttons: Optional[list] = None): + if default_url: + default_action = { + "type": "web_url", + "url": default_url, + "messenger_extensions": "false", + "webview_height_ratio": "tall" + } + else: + default_action = None + + header = {"Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}"} + body = { + "recipient": { + "id": sender_id + }, + "message": { + "attachment": { + "type": "template", + "payload": { + "template_type": "generic", + "elements": [ + { + "title": title, + "image_url": image_url if image_url else "", + "subtitle": subtitle if subtitle else "", + "default_action": default_action, + "buttons": buttons if buttons else [] + } + ] + } + } + } + } + + try: + r = requests.post(self._url, headers=header, json=body, timeout=10) + r.raise_for_status() + return r.json() + except requests.exceptions.RequestException as e: + print(f"Request failed: {e} \n{r.json()}") + return None + + def send_receipt_template(self, sender_id: str, order_number: str, payment_method: str, summary: dict, currency: str = 'USD', + order_url: Optional[str] = None, timestamp: Optional[str] = None, address: Optional[dict] = None, + adjustments: Optional[list] = None, elements: Optional[list] = None): + header = {"Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}"} + body = { + "recipient": { + "id": sender_id + }, + "message": { + "attachment": { + "type": "template", + "payload": { + "template_type": "receipt", + "recipient_name": "Stephane Crozatier", + "order_number": order_number, + "currency": currency, + "payment_method": payment_method, + "order_url": order_url, + "timestamp": timestamp, + "address": address, + "summary": summary, + "adjustments": adjustments, + "elements": elements + } + } + } + } + + try: + r = requests.post(self._url, headers=header, json=body, timeout=10) + r.raise_for_status() + return r.json() + except requests.exceptions.RequestException as e: + print(f"Request failed: {e} \n{r.json()}") + return None diff --git a/pynani/__init__.py b/pynani/__init__.py new file mode 100644 index 0000000..5c267d1 --- /dev/null +++ b/pynani/__init__.py @@ -0,0 +1,4 @@ +from .Messenger import Messenger +from .utils.QuickReply import QuickReply +from .utils.Buttons import Buttons +from .utils import elements diff --git a/pynani/utils/Buttons.py b/pynani/utils/Buttons.py new file mode 100644 index 0000000..a209955 --- /dev/null +++ b/pynani/utils/Buttons.py @@ -0,0 +1,52 @@ +from typing import Optional, Union + + +class Buttons: + def __make_button(self, title: str, url: Optional[str] = None, call_number: Optional[str] = None) -> dict: + if url is not None and call_number is not None: + raise ValueError("You can't have both url and call_number at the same time") + + if url: + type_button = "web_url" + url_button = url + payload_button = "" + elif call_number: + type_button = "phone_number" + url_button = "" + payload_button = call_number + elif not url and not call_number: + type_button = "postback" + url_button = "" + payload_button = "DEVELOPER_DEFINED_PAYLOAD" + + return { + "type": type_button, + "title": title, + "payload": payload_button, + "url": url_button, + } + + def basic_buttons(self, buttons: Union[str, list]) -> list: + if isinstance(buttons, str): + return [self.__make_button(buttons)] + else: + if len(buttons) > 3: + print("Buttons template should be less than 3") + buttons = buttons[:3] + return [self.__make_button(button) for button in buttons] + + def leave_buttons(self, buttons: Union[dict, list]): + if isinstance(buttons, dict): + return [self.__make_button(**buttons)] + else: + if len(buttons) > 3: + print("Buttons template should be less than 3") + buttons = buttons[:3] + return [self.__make_button(**button) for button in buttons] + + +# bb = Button() +# # print(bb.basic_buttons(["Hola", "Mundo", "馃敟"])) +# print(bb.leave_buttons([{"title": "Hola", "url": "https://www.google.com"}, {"title": "Mundo", "call_number": "+525555555555"}])) + + diff --git a/pynani/utils/QuickReply.py b/pynani/utils/QuickReply.py new file mode 100644 index 0000000..d07c5e5 --- /dev/null +++ b/pynani/utils/QuickReply.py @@ -0,0 +1,42 @@ +class QuickReply(): + def __make_quick_button(self, text: str, payload: str = "", image_url: str = None) -> dict: + return { + "content_type": "text", + "title": text, + "payload": payload, + "image_url": image_url if image_url else "", + } + + def quick_buttons(self, buttons: list) -> list: + r_buttons = [] + if len(buttons) > 13: + print("Quick replies should be less than 13") + buttons = buttons[:13] + + for b in buttons: + if not isinstance(b, str): + raise ValueError("Each button should be a string") + else: + r_buttons.append(self.__make_quick_button(b)) + + return r_buttons + + def quick_buttons_image(self, buttons: list) -> list: + r_buttons = [] + if len(buttons) > 13: + print("Quick replies should be less than 13") + buttons = buttons[:13] + for b in buttons: + if not isinstance(b, dict): + raise ValueError("Each button should be a dictionary") + else: + r_buttons.append(self.__make_quick_button(**b)) + + return r_buttons + + +# botones = ['pedro', 'juan', 'popo'] +# b_imagen = [{"text": "1", "image_url": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Solid_red.png"}, +# {"text": "2", "image_url": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Solid_red.png"}] +# sb = qui.quick_buttons(botones) +# cb = qui.quick_buttons_image(b_imagen) diff --git a/pynani/utils/elements.py b/pynani/utils/elements.py new file mode 100644 index 0000000..6442746 --- /dev/null +++ b/pynani/utils/elements.py @@ -0,0 +1,44 @@ +from typing import Optional, List, Any + + +def get_address(street_1: str, city: str, postal_code: str, state: str, country: str, street_2: Optional[str] = None) -> dict: + return { + "street_1": street_1, + "street_2": street_2, + "city": city, + "postal_code": postal_code, + "state": state, + "country": country + } + + +def get_summary(total_cost: float, subtotal: Optional[float] = None, shipping_cost: Optional[float] = None, total_tax: Optional[float] = None): + return { + "subtotal": subtotal, + "shipping_cost": shipping_cost, + "total_tax": total_tax, + "total_cost": total_cost + } + + +def get_adjustments(*args: Any) -> list: + adjustments = [] + for i in range(0, len(args), 2): + adjustment = { + "name": args[i], + "amount": args[i + 1] + } + adjustments.append(adjustment) + return adjustments + + +def get_elements(title: str, price: float, subtitle: Optional[str] = None, quantity: Optional[int] = None, currency: Optional[str] = 'USD', image_url: Optional[str] = None) -> List[dict]: + item = { + "title": title, + "subtitle": subtitle, + "quantity": quantity, + "price": price, + "currency": currency, + "image_url": image_url + } + return [item] From b3bf2874cbf10ebaa0b25654d1dd8ffb2afbc158 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Sun, 23 Jun 2024 02:12:14 +0000 Subject: [PATCH 06/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f3b53b..5ad7f8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,8 +17,6 @@ classifiers = [ "Topic :: Utilities", "License :: OSI Approved :: MIT License", # "Topic :: Documentation :: Sphinx" "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -36,7 +34,7 @@ license = {file = "LICENSE"} name = "pynani" readme = "README.md" requires-python = ">=3.8" -version = "1.2.0" +version = "1.2.1" [project.urls] "Bug Tracker" = "https://github.com/jorge-jrzz/Pynani/issues" From 8bc6f44bcf22bbfd566c2d72f93614223446a7b4 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Sun, 23 Jun 2024 05:47:23 +0000 Subject: [PATCH 07/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/Messenger.py | 335 ++++++++++++++++++++++++++++++++------- pynani/verify_token.html | 34 ++++ 2 files changed, 312 insertions(+), 57 deletions(-) create mode 100644 pynani/verify_token.html diff --git a/pynani/Messenger.py b/pynani/Messenger.py index ac9cbb0..0cb9233 100644 --- a/pynani/Messenger.py +++ b/pynani/Messenger.py @@ -1,34 +1,116 @@ -from typing import Union, Optional -from pathlib import Path +# 鈻堚枅鈻堚枅鈻堚枅鈺椻枒鈻堚枅鈺椻枒鈻戔枒鈻堚枅鈺椻枅鈻堚枅鈺椻枒鈻戔枅鈻堚晽鈻戔枅鈻堚枅鈻堚枅鈺椻枒鈻堚枅鈻堚晽鈻戔枒鈻堚枅鈺椻枅鈻堚晽 +# 鈻堚枅鈺斺晲鈺愨枅鈻堚晽鈺氣枅鈻堚晽鈻戔枅鈻堚晹鈺濃枅鈻堚枅鈻堚晽鈻戔枅鈻堚晳鈻堚枅鈺斺晲鈺愨枅鈻堚晽鈻堚枅鈻堚枅鈺椻枒鈻堚枅鈺戔枅鈻堚晳 +# 鈻堚枅鈻堚枅鈻堚枅鈺斺暆鈻戔暁鈻堚枅鈻堚枅鈺斺暆鈻戔枅鈻堚晹鈻堚枅鈺椻枅鈻堚晳鈻堚枅鈻堚枅鈻堚枅鈻堚晳鈻堚枅鈺斺枅鈻堚晽鈻堚枅鈺戔枅鈻堚晳 +# 鈻堚枅鈺斺晲鈺愨晲鈺濃枒鈻戔枒鈺氣枅鈻堚晹鈺濃枒鈻戔枅鈻堚晳鈺氣枅鈻堚枅鈻堚晳鈻堚枅鈺斺晲鈺愨枅鈻堚晳鈻堚枅鈺戔暁鈻堚枅鈻堚枅鈺戔枅鈻堚晳 +# 鈻堚枅鈺戔枒鈻戔枒鈻戔枒鈻戔枒鈻戔枅鈻堚晳鈻戔枒鈻戔枅鈻堚晳鈻戔暁鈻堚枅鈻堚晳鈻堚枅鈺戔枒鈻戔枅鈻堚晳鈻堚枅鈺戔枒鈺氣枅鈻堚枅鈺戔枅鈻堚晳 +# 鈺氣晲鈺濃枒鈻戔枒鈻戔枒鈻戔枒鈻戔暁鈺愨暆鈻戔枒鈻戔暁鈺愨暆鈻戔枒鈺氣晲鈺愨暆鈺氣晲鈺濃枒鈻戔暁鈺愨暆鈺氣晲鈺濃枒鈻戔暁鈺愨晲鈺濃暁鈺愨暆 + + import mimetypes +import json +import logging +from pathlib import Path +from typing import Union, Optional, Tuple, Dict import requests +from requests.exceptions import RequestException + + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s from Pynani - %(message)s' +) + + +def jsonify(data: Union[Dict, str], status_code: int) -> Tuple[Dict, int, Dict]: + """ + Converts the given data to a JSON response with the specified status code. + + Args: + data (Union[Dict, str]): The data to be converted to JSON. It can be a dictionary or a string. + status_code (int): The HTTP status code to be returned with the response. + + Returns: + Tuple[Dict, int, Dict]: A tuple containing the JSON response, the status code, and the headers. + """ + + if isinstance(data, dict): + return json.dumps(data), status_code, {'Content-Type': 'application/json'} + elif isinstance(data, str): + return data.encode('utf-8'), status_code, {'Content-Type': 'text/html'} class Messenger(): - def __init__(self, access_token: str, page_id: str = 'me'): + """ + Initializes the Messenger class with the provided access token and page ID. + + Args: + access_token (str): The access token for authenticating API requests. + page_id (str, optional): The page ID for the Facebook page. Defaults to 'me'. + """ + + def __init__(self, access_token: str, page_id: str = 'me') -> None: self.access_token = access_token self.page_id = page_id - self._url = f"https://graph.facebook.com/v20.0/{page_id}/messages" + self.__url = f"https://graph.facebook.com/v20.0/{page_id}/messages" + + + def verify_token(self, params: Dict, token: str) -> Tuple[Dict, int, Dict]: + """ + Verifies the provided token against the expected token. + + Args: + params (Dict): The parameters received in the verification request. + token (str): The expected verification token. + + Returns: + Tuple[Dict, int, Dict]: A tuple containing the JSON response, the status code, and the headers. + """ - def verify_token(self, params, token): mode = params.get("hub.mode") hub_token = params.get("hub.verify_token") challenge = params.get("hub.challenge") if mode == "subscribe" and challenge: if hub_token != token: - return "Verification token mismatch", 403 - return challenge, 200 - return "Hello world", 200 + logging.info('Response: %d - %s', 403, + 'Verification token mismatch') + return jsonify("Verification token mismatch", 403) + logging.info('Response: %d - %s', 200, "Verification successful") + return jsonify(challenge, 200) + logging.info('Response: %d - %s', 200, + "This endpoint is to verify token") + return jsonify(Path("pynani/verify_token.html").read_text(encoding='utf-8'), 200) + + + def get_sender_id(self, data: dict) -> Optional[str]: + """ + Extracts the sender ID from the provided data. + + Args: + data (dict): The data received from the webhook event. + + Returns: + Optional[str]: The sender ID if found, otherwise None. + """ - def get_sender_id(self, data: dict): try: return data['entry'][0]['messaging'][0]['sender']['id'] except (IndexError, KeyError) as e: - print(f"Error accessing sender ID: {e}") + logging.info("Error accessing sender ID: %s", e) return None - def get_message_type(self, data: dict): + + def get_message_type(self, data: Dict) -> Optional[str]: + """ + Determines the type of message received from the webhook event. + + Args: + data (Dict): The data received from the webhook event. + + Returns: + Optional[str]: The type of message if found, otherwise None. + """ + messaging = data['entry'][0]['messaging'][0] try: if 'postback' in messaging: @@ -48,25 +130,49 @@ def get_message_type(self, data: dict): else: return attachment_type except (IndexError, KeyError) as e: - print(f"Error accessing message type: {e}") + logging.info("Error accessing message type: %s", e) + # print(f"Error accessing message type: {e}") return None - def get_message_text(self, data: dict): + + def get_message_text(self, data: Dict) -> Optional[str]: + """ + Extracts the text message from the provided data. + + Args: + data (Dict): The data received from the webhook event. + + Returns: + Optional[str]: The text message if found, otherwise None. + """ + try: message = data['entry'][0]['messaging'][0] if 'message' in message: - # print("Hola?") return message['message']['text'] elif 'postback' in message: return message['postback']['title'] except (IndexError, KeyError) as e: - print(f"Error accessing message text: {e}") + logging.info("Error accessing message text: %s", e) + # print(f"Error accessing message text: {e}") return None - def send_text_message(self, sender_id, message: Union[str, int]): + + def send_text_message(self, sender_id: str, message: Union[str, int]) -> Optional[Dict]: + """ + Sends a text message to the specified sender. + + Args: + sender_id (str): The ID of the recipient. + message (Union[str, int]): The message to be sent. + + Returns: + Optional[Dict]: The response from the server if the request was successful, otherwise None. + """ + header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} - payload = { + body = { "recipient": { "id": sender_id }, @@ -77,17 +183,29 @@ def send_text_message(self, sender_id, message: Union[str, int]): } try: - r = requests.post(self._url, headers=header, - json=payload, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() - return r.json() - except requests.exceptions.RequestException as e: - print(f"Request failed: {e} \n{r.json()}") + logging.info("Response: %d - %s", 200, "Message sent successfully") + return jsonify(r.json(), 200) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) + # print(f"Request failed: {e} \n{r.json()}") return None + def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: - attachments_url = f"https://graph.facebook.com/v20.0/{ - self.page_id}/message_attachments" + """ + Uploads an attachment to the server and returns the attachment ID. + + Args: + attachment_type (str): The type of the attachment (e.g., 'image', 'video', 'audio', 'file'). + attachment_path (str): The local file path to the attachment. + + Returns: + str: The ID of the uploaded attachment if successful, otherwise None. + """ + + attachments_url = f"https://graph.facebook.com/v20.0/{self.page_id}/message_attachments" attachment = Path(attachment_path) mimetype, _ = mimetypes.guess_type(attachment) @@ -107,34 +225,74 @@ def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: } body = {"message": str(message)} - r = requests.post(attachments_url, headers=header, - files=file, data=body, timeout=20) - try: + r = requests.post(attachments_url, headers=header, + files=file, data=body, timeout=20) + r.raise_for_status() + logging.info("Response: %d - %s", 200, + "Attachment uploaded successfully") attachment_id = r.json()["attachment_id"] return attachment_id - except KeyError as e: - print(f"Error uploading attachment: {e}") + except (RequestException, IndexError, KeyError) as e: + logging.info("Response: %d - %s", 403, e) + # print(f"Request failed: {e} \n{r.json()}") return None - def get_url_attachment(self, data: dict): + + def get_url_attachment(self, data: Dict) -> Optional[str]: + """ + Extracts the URL of an attachment from the provided data. + + Args: + data (Dict): The data containing the attachment information. + + Returns: + Optional[str]: The URL of the attachment if found, otherwise None. + """ + try: return data['entry'][0]['messaging'][0]['message']['attachments'][0]["payload"]["url"] except (IndexError, KeyError) as e: - print(f"Error accessing attachment url: {e}") + logging.info("Error accessing attachment url: %s", e) + # print(f"Error accessing attachment url: {e}") return None - def get_attachment_type(self, data: dict): + + def get_attachment_type(self, data: Dict) -> Optional[str]: + """ + Extracts the type of an attachment from the provided data. + + Args: + data (Dict): The data containing the attachment information. + + Returns: + Optional[str]: The type of the attachment if found, otherwise None. + """ + try: return data['entry'][0]['messaging'][0]['message']['attachments'][0]["type"] except (IndexError, KeyError) as e: - print(f"Error accessing attachment type: {e}") + logging.info("Error accessing attachment type: %s", e) + # print(f"Error accessing attachment type: {e}") return None - def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: str): + + def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: str) -> Optional[Dict]: + """ + Sends an attachment to a user. + + Args: + sender_id (str): The ID of the recipient. + attachment_type (str): The type of the attachment (e.g., 'image', 'video', 'audio', 'file'). + attachment_url (str): The URL of the attachment to be sent. + + Returns: + Optional[Dict]: The response from the server if the request is successful, otherwise None. + """ + header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} - payload = { + body = { "recipient": { "id": sender_id }, @@ -150,12 +308,33 @@ def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: } } - r = requests.post(self._url, headers=header, json=payload, timeout=10) - return r.json() + try: + r = requests.post(self.__url, headers=header, json=body, timeout=15) + r.raise_for_status() + logging.info("Response: %d - %s", 200, "Attachment sent successfully") + return jsonify(r.json(), 200) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) + # print(f"Request failed: {e} \n{r.json()}") + return None + + + def send_local_attachment(self, sender_id: str, attachment_type: str, attachment_path: str) -> Optional[Dict]: + """ + Sends a local attachment to a user. + + Args: + sender_id (str): The ID of the recipient. + attachment_type (str): The type of the attachment (e.g., 'image', 'video', 'audio', 'file'). + attachment_path (str): The local path to the attachment to be sent. + + Returns: + Optional[Dict]: The response from the server if the request is successful, otherwise None. + """ - def send_local_attachment(self, sender_id: str, attachment_type: str, attachment_path: str): attachment = Path(attachment_path) mimetype, _ = mimetypes.guess_type(attachment) + recipient = {"id": sender_id} message = { "attachment": { @@ -175,28 +354,62 @@ def send_local_attachment(self, sender_id: str, attachment_type: str, attachment "filedata": (attachment.name, attachment.open('rb'), mimetype) } - r = requests.post(self._url, headers=header, - data=body, files=file, timeout=10) - return r.json() + try: + r = requests.post(self.__url, headers=header, data=body, files=file, timeout=15) + r.raise_for_status() + logging.info("Response: %d - %s", 200, "Attachment sent successfully") + return jsonify(r.json(), 200) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) + # print(f"Request failed: {e} \n{r.json()}") + return None + + + def download_attachment(self, attachment_url: str, path_dest: str) -> None: + """ + Downloads an attachment from the given URL and saves it to the specified destination path. - def download_attachment(self, attachment_url: str, path_dest: str): - response = requests.get(attachment_url, stream=True, timeout=10) - if response.status_code == 200: + Args: + attachment_url (str): The URL of the attachment to be downloaded. + path_dest (str): The local file path where the attachment will be saved. + + Returns: + None + """ + + try: + r = requests.get(attachment_url, stream=True, timeout=10) + r.raise_for_status() with open(path_dest, 'wb') as file: - for chunk in response.iter_content(1024): + for chunk in r.iter_content(1024): file.write(chunk) + logging.info("Response: %d - Downloaded attachment successfully to %s", 200, path_dest) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) + return None - print('Downloaded attachment successfully!') - else: - print('Error downloading attachment') - def send_quick_reply(self, sender_id, message: str, quick_replies: list): + def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_replies: list) -> Optional[Dict]: + """ + Sends a quick reply message to the specified sender. + + Args: + sender_id (str): The ID of the recipient. + message (Union[str, int]): The message to be sent. + quick_replies (list): A list of quick reply options. The list should contain less than 13 items. + + Returns: + Optional[Dict]: The response from the server if the request was successful, otherwise None. + """ + if len(quick_replies) > 13: - print("Quick replies should be less than 13") + logging.info("Quick replies should be less than 13") + # print("Quick replies should be less than 13") quick_replies = quick_replies[:13] + header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} - payload = { + body = { "recipient": { "id": sender_id }, @@ -207,8 +420,14 @@ def send_quick_reply(self, sender_id, message: str, quick_replies: list): } } - r = requests.post(self._url, headers=header, json=payload, timeout=10) - return r.json() + try: + r = requests.post(self.__url, headers=header, json=body, timeout=10) + r.raise_for_status() + logging.info("Response: %d - %s", 200, "Quick reply sent successfully") + return jsonify(r.json(), 200) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) + return None def send_button_template(self, sender_id: str, message: str, buttons: list): header = {"Content-Type": "application/json", @@ -230,7 +449,7 @@ def send_button_template(self, sender_id: str, message: str, buttons: list): } } - r = requests.post(self._url, headers=header, json=payload, timeout=10) + r = requests.post(self.__url, headers=header, json=payload, timeout=10) return r.json() def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: list): @@ -258,7 +477,7 @@ def send_media_template(self, sender_id: str, media_type: str, attachment_id: st } } - r = requests.post(self._url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) return r.json() def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, subtitle: Optional[str] = None, buttons: Optional[list] = None): @@ -298,7 +517,8 @@ def send_generic_template(self, sender_id: str, title: str, image_url: Optional[ } try: - r = requests.post(self._url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, + json=body, timeout=10) r.raise_for_status() return r.json() except requests.exceptions.RequestException as e: @@ -335,7 +555,8 @@ def send_receipt_template(self, sender_id: str, order_number: str, payment_metho } try: - r = requests.post(self._url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, + json=body, timeout=10) r.raise_for_status() return r.json() except requests.exceptions.RequestException as e: diff --git a/pynani/verify_token.html b/pynani/verify_token.html new file mode 100644 index 0000000..42ad488 --- /dev/null +++ b/pynani/verify_token.html @@ -0,0 +1,34 @@ + + + + + + Verificaci贸n de Token + + + +
+

Hello, World!

+

This is the endpoint to verify the token 馃攼馃敆

+
+ + From 7265c0b8ad587196a25678d1e1c0a10514f1335e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20=C3=81ngel=20Ju=C3=A1rez=20V=C3=A1zquez?= Date: Sat, 22 Jun 2024 23:49:47 -0600 Subject: [PATCH 08/22] Delete Pynani directory --- Pynani/Messenger.py | 343 ------------------------------------- Pynani/__init__.py | 4 - Pynani/utils/Buttons.py | 52 ------ Pynani/utils/QuickReply.py | 42 ----- Pynani/utils/elements.py | 44 ----- 5 files changed, 485 deletions(-) delete mode 100644 Pynani/Messenger.py delete mode 100644 Pynani/__init__.py delete mode 100644 Pynani/utils/Buttons.py delete mode 100644 Pynani/utils/QuickReply.py delete mode 100644 Pynani/utils/elements.py diff --git a/Pynani/Messenger.py b/Pynani/Messenger.py deleted file mode 100644 index ac9cbb0..0000000 --- a/Pynani/Messenger.py +++ /dev/null @@ -1,343 +0,0 @@ -from typing import Union, Optional -from pathlib import Path -import mimetypes -import requests - - -class Messenger(): - def __init__(self, access_token: str, page_id: str = 'me'): - self.access_token = access_token - self.page_id = page_id - self._url = f"https://graph.facebook.com/v20.0/{page_id}/messages" - - def verify_token(self, params, token): - mode = params.get("hub.mode") - hub_token = params.get("hub.verify_token") - challenge = params.get("hub.challenge") - - if mode == "subscribe" and challenge: - if hub_token != token: - return "Verification token mismatch", 403 - return challenge, 200 - return "Hello world", 200 - - def get_sender_id(self, data: dict): - try: - return data['entry'][0]['messaging'][0]['sender']['id'] - except (IndexError, KeyError) as e: - print(f"Error accessing sender ID: {e}") - return None - - def get_message_type(self, data: dict): - messaging = data['entry'][0]['messaging'][0] - try: - if 'postback' in messaging: - return 'postback' - message_type = messaging['message'] - if 'text' in message_type: - if 'attachments' in message_type: - if message_type['attachments'][0]['type'] == 'fallback': - return 'link' - return 'text' - if 'attachments' in message_type: - attachment_type = message_type['attachments'][0]['type'] - if 'image' in attachment_type: - if 'sticker_id' in message_type['attachments'][0]['payload']: - return 'sticker' - return 'image' - else: - return attachment_type - except (IndexError, KeyError) as e: - print(f"Error accessing message type: {e}") - return None - - def get_message_text(self, data: dict): - try: - message = data['entry'][0]['messaging'][0] - if 'message' in message: - # print("Hola?") - return message['message']['text'] - elif 'postback' in message: - return message['postback']['title'] - except (IndexError, KeyError) as e: - print(f"Error accessing message text: {e}") - return None - - def send_text_message(self, sender_id, message: Union[str, int]): - header = {"Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}"} - payload = { - "recipient": { - "id": sender_id - }, - "messaging_type": "RESPONSE", - "message": { - "text": message - } - } - - try: - r = requests.post(self._url, headers=header, - json=payload, timeout=10) - r.raise_for_status() - return r.json() - except requests.exceptions.RequestException as e: - print(f"Request failed: {e} \n{r.json()}") - return None - - def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: - attachments_url = f"https://graph.facebook.com/v20.0/{ - self.page_id}/message_attachments" - attachment = Path(attachment_path) - mimetype, _ = mimetypes.guess_type(attachment) - - header = { - "Authorization": f"Bearer {self.access_token}" - } - message = { - "attachment": { - "type": attachment_type, - "payload": { - "is_reusable": "true" - } - } - } - file = { - "filedata": (attachment.name, attachment.open('rb'), mimetype) - } - body = {"message": str(message)} - - r = requests.post(attachments_url, headers=header, - files=file, data=body, timeout=20) - - try: - attachment_id = r.json()["attachment_id"] - return attachment_id - except KeyError as e: - print(f"Error uploading attachment: {e}") - return None - - def get_url_attachment(self, data: dict): - try: - return data['entry'][0]['messaging'][0]['message']['attachments'][0]["payload"]["url"] - except (IndexError, KeyError) as e: - print(f"Error accessing attachment url: {e}") - return None - - def get_attachment_type(self, data: dict): - try: - return data['entry'][0]['messaging'][0]['message']['attachments'][0]["type"] - except (IndexError, KeyError) as e: - print(f"Error accessing attachment type: {e}") - return None - - def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: str): - header = {"Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}"} - payload = { - "recipient": { - "id": sender_id - }, - "messaging_type": "RESPONSE", - "message": { - "attachment": { - "type": attachment_type, - "payload": { - "url": attachment_url, - "is_reusable": True - } - } - } - } - - r = requests.post(self._url, headers=header, json=payload, timeout=10) - return r.json() - - def send_local_attachment(self, sender_id: str, attachment_type: str, attachment_path: str): - attachment = Path(attachment_path) - mimetype, _ = mimetypes.guess_type(attachment) - recipient = {"id": sender_id} - message = { - "attachment": { - "type": attachment_type, - "payload": { - "is_reusable": "true" - } - } - } - - header = {"Authorization": f"Bearer {self.access_token}"} - body = { - "recipient": str(recipient), - "message": str(message) - } - file = { - "filedata": (attachment.name, attachment.open('rb'), mimetype) - } - - r = requests.post(self._url, headers=header, - data=body, files=file, timeout=10) - return r.json() - - def download_attachment(self, attachment_url: str, path_dest: str): - response = requests.get(attachment_url, stream=True, timeout=10) - if response.status_code == 200: - with open(path_dest, 'wb') as file: - for chunk in response.iter_content(1024): - file.write(chunk) - - print('Downloaded attachment successfully!') - else: - print('Error downloading attachment') - - def send_quick_reply(self, sender_id, message: str, quick_replies: list): - if len(quick_replies) > 13: - print("Quick replies should be less than 13") - quick_replies = quick_replies[:13] - header = {"Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}"} - payload = { - "recipient": { - "id": sender_id - }, - "messaging_type": "RESPONSE", - "message": { - "text": message, - "quick_replies": quick_replies - } - } - - r = requests.post(self._url, headers=header, json=payload, timeout=10) - return r.json() - - def send_button_template(self, sender_id: str, message: str, buttons: list): - header = {"Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}"} - payload = { - "recipient": { - "id": sender_id - }, - "messaging_type": "RESPONSE", - "message": { - "attachment": { - "type": "template", - "payload": { - "template_type": "button", - "text": message, - "buttons": buttons - } - } - } - } - - r = requests.post(self._url, headers=header, json=payload, timeout=10) - return r.json() - - def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: list): - header = {"Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}"} - body = { - "recipient": { - "id": sender_id - }, - "messaging_type": "RESPONSE", - "message": { - "attachment": { - "type": "template", - "payload": { - "template_type": "media", - "elements": [ - { - "media_type": media_type, - "attachment_id": attachment_id, - "buttons": buttons - } - ] - } - } - } - } - - r = requests.post(self._url, headers=header, json=body, timeout=10) - return r.json() - - def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, subtitle: Optional[str] = None, buttons: Optional[list] = None): - if default_url: - default_action = { - "type": "web_url", - "url": default_url, - "messenger_extensions": "false", - "webview_height_ratio": "tall" - } - else: - default_action = None - - header = {"Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}"} - body = { - "recipient": { - "id": sender_id - }, - "message": { - "attachment": { - "type": "template", - "payload": { - "template_type": "generic", - "elements": [ - { - "title": title, - "image_url": image_url if image_url else "", - "subtitle": subtitle if subtitle else "", - "default_action": default_action, - "buttons": buttons if buttons else [] - } - ] - } - } - } - } - - try: - r = requests.post(self._url, headers=header, json=body, timeout=10) - r.raise_for_status() - return r.json() - except requests.exceptions.RequestException as e: - print(f"Request failed: {e} \n{r.json()}") - return None - - def send_receipt_template(self, sender_id: str, order_number: str, payment_method: str, summary: dict, currency: str = 'USD', - order_url: Optional[str] = None, timestamp: Optional[str] = None, address: Optional[dict] = None, - adjustments: Optional[list] = None, elements: Optional[list] = None): - header = {"Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}"} - body = { - "recipient": { - "id": sender_id - }, - "message": { - "attachment": { - "type": "template", - "payload": { - "template_type": "receipt", - "recipient_name": "Stephane Crozatier", - "order_number": order_number, - "currency": currency, - "payment_method": payment_method, - "order_url": order_url, - "timestamp": timestamp, - "address": address, - "summary": summary, - "adjustments": adjustments, - "elements": elements - } - } - } - } - - try: - r = requests.post(self._url, headers=header, json=body, timeout=10) - r.raise_for_status() - return r.json() - except requests.exceptions.RequestException as e: - print(f"Request failed: {e} \n{r.json()}") - return None diff --git a/Pynani/__init__.py b/Pynani/__init__.py deleted file mode 100644 index 5c267d1..0000000 --- a/Pynani/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .Messenger import Messenger -from .utils.QuickReply import QuickReply -from .utils.Buttons import Buttons -from .utils import elements diff --git a/Pynani/utils/Buttons.py b/Pynani/utils/Buttons.py deleted file mode 100644 index a209955..0000000 --- a/Pynani/utils/Buttons.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Optional, Union - - -class Buttons: - def __make_button(self, title: str, url: Optional[str] = None, call_number: Optional[str] = None) -> dict: - if url is not None and call_number is not None: - raise ValueError("You can't have both url and call_number at the same time") - - if url: - type_button = "web_url" - url_button = url - payload_button = "" - elif call_number: - type_button = "phone_number" - url_button = "" - payload_button = call_number - elif not url and not call_number: - type_button = "postback" - url_button = "" - payload_button = "DEVELOPER_DEFINED_PAYLOAD" - - return { - "type": type_button, - "title": title, - "payload": payload_button, - "url": url_button, - } - - def basic_buttons(self, buttons: Union[str, list]) -> list: - if isinstance(buttons, str): - return [self.__make_button(buttons)] - else: - if len(buttons) > 3: - print("Buttons template should be less than 3") - buttons = buttons[:3] - return [self.__make_button(button) for button in buttons] - - def leave_buttons(self, buttons: Union[dict, list]): - if isinstance(buttons, dict): - return [self.__make_button(**buttons)] - else: - if len(buttons) > 3: - print("Buttons template should be less than 3") - buttons = buttons[:3] - return [self.__make_button(**button) for button in buttons] - - -# bb = Button() -# # print(bb.basic_buttons(["Hola", "Mundo", "馃敟"])) -# print(bb.leave_buttons([{"title": "Hola", "url": "https://www.google.com"}, {"title": "Mundo", "call_number": "+525555555555"}])) - - diff --git a/Pynani/utils/QuickReply.py b/Pynani/utils/QuickReply.py deleted file mode 100644 index d07c5e5..0000000 --- a/Pynani/utils/QuickReply.py +++ /dev/null @@ -1,42 +0,0 @@ -class QuickReply(): - def __make_quick_button(self, text: str, payload: str = "", image_url: str = None) -> dict: - return { - "content_type": "text", - "title": text, - "payload": payload, - "image_url": image_url if image_url else "", - } - - def quick_buttons(self, buttons: list) -> list: - r_buttons = [] - if len(buttons) > 13: - print("Quick replies should be less than 13") - buttons = buttons[:13] - - for b in buttons: - if not isinstance(b, str): - raise ValueError("Each button should be a string") - else: - r_buttons.append(self.__make_quick_button(b)) - - return r_buttons - - def quick_buttons_image(self, buttons: list) -> list: - r_buttons = [] - if len(buttons) > 13: - print("Quick replies should be less than 13") - buttons = buttons[:13] - for b in buttons: - if not isinstance(b, dict): - raise ValueError("Each button should be a dictionary") - else: - r_buttons.append(self.__make_quick_button(**b)) - - return r_buttons - - -# botones = ['pedro', 'juan', 'popo'] -# b_imagen = [{"text": "1", "image_url": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Solid_red.png"}, -# {"text": "2", "image_url": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Solid_red.png"}] -# sb = qui.quick_buttons(botones) -# cb = qui.quick_buttons_image(b_imagen) diff --git a/Pynani/utils/elements.py b/Pynani/utils/elements.py deleted file mode 100644 index 6442746..0000000 --- a/Pynani/utils/elements.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Optional, List, Any - - -def get_address(street_1: str, city: str, postal_code: str, state: str, country: str, street_2: Optional[str] = None) -> dict: - return { - "street_1": street_1, - "street_2": street_2, - "city": city, - "postal_code": postal_code, - "state": state, - "country": country - } - - -def get_summary(total_cost: float, subtotal: Optional[float] = None, shipping_cost: Optional[float] = None, total_tax: Optional[float] = None): - return { - "subtotal": subtotal, - "shipping_cost": shipping_cost, - "total_tax": total_tax, - "total_cost": total_cost - } - - -def get_adjustments(*args: Any) -> list: - adjustments = [] - for i in range(0, len(args), 2): - adjustment = { - "name": args[i], - "amount": args[i + 1] - } - adjustments.append(adjustment) - return adjustments - - -def get_elements(title: str, price: float, subtitle: Optional[str] = None, quantity: Optional[int] = None, currency: Optional[str] = 'USD', image_url: Optional[str] = None) -> List[dict]: - item = { - "title": title, - "subtitle": subtitle, - "quantity": quantity, - "price": price, - "currency": currency, - "image_url": image_url - } - return [item] From ec1ce278293187ede4a95a3144a6153f8678d3a2 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Sun, 23 Jun 2024 18:30:22 +0000 Subject: [PATCH 09/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/Messenger.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/pynani/Messenger.py b/pynani/Messenger.py index 0cb9233..bf3e440 100644 --- a/pynani/Messenger.py +++ b/pynani/Messenger.py @@ -10,7 +10,7 @@ import json import logging from pathlib import Path -from typing import Union, Optional, Tuple, Dict +from typing import Union, Optional, Tuple, Dict, List import requests from requests.exceptions import RequestException @@ -389,7 +389,7 @@ def download_attachment(self, attachment_url: str, path_dest: str) -> None: return None - def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_replies: list) -> Optional[Dict]: + def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_replies: List[Dict]) -> Optional[Dict]: """ Sends a quick reply message to the specified sender. @@ -429,10 +429,26 @@ def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_repli logging.info("Response: %d - %s", 403, e) return None - def send_button_template(self, sender_id: str, message: str, buttons: list): + def send_button_template(self, sender_id: str, message: str, buttons: List[Dict]) -> Optional[Dict]: + """ + Sends a button template message to the specified sender. + + Args: + sender_id (str): The ID of the recipient. + message (str): The message to be sent. + buttons (list): A list of button options. The list should contain less than 3 items. + + Returns: + Optional[Dict]: The response from the server if the request was successful, otherwise None. + """ + + if len(buttons) > 3: + logging.info("Buttons template should be less than 3") + buttons = buttons[:3] + header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} - payload = { + body = { "recipient": { "id": sender_id }, @@ -449,8 +465,15 @@ def send_button_template(self, sender_id: str, message: str, buttons: list): } } - r = requests.post(self.__url, headers=header, json=payload, timeout=10) - return r.json() + try: + r = requests.post(self.__url, headers=header, json=body, timeout=10) + r.raise_for_status() + logging.info("Response: %d - %s", 200, "Button template sent successfully") + return jsonify(r.json(), 200) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) + return None + def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: list): header = {"Content-Type": "application/json", From d8b663e68b63cb46cfe3027c5cef75bd4011dba2 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Sun, 23 Jun 2024 23:28:33 +0000 Subject: [PATCH 10/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/Messenger.py | 90 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 17 deletions(-) diff --git a/pynani/Messenger.py b/pynani/Messenger.py index bf3e440..dc1475e 100644 --- a/pynani/Messenger.py +++ b/pynani/Messenger.py @@ -475,7 +475,20 @@ def send_button_template(self, sender_id: str, message: str, buttons: List[Dict] return None - def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: list): + def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: List[Dict]) -> Optional[Dict]: + """ + Sends a media template message to the specified sender. + + Args: + sender_id (str): The ID of the recipient. + media_type (str): The type of media to be sent (e.g., 'image', 'video', 'audio', 'file'). + attachment_id (str): The ID of the attachment to be sent. + buttons (list): A list of button options. The list should contain less than 3 items. + + Returns: + Optional[Dict]: The response from the server if the request was successful, otherwise None. + """ + header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} body = { @@ -500,10 +513,33 @@ def send_media_template(self, sender_id: str, media_type: str, attachment_id: st } } - r = requests.post(self.__url, headers=header, json=body, timeout=10) - return r.json() + try: + r = requests.post(self.__url, headers=header, json=body, timeout=10) + r.raise_for_status() + logging.info("Response: %d - %s", 200, "Media template sent successfully") + return jsonify(r.json(), 200) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) + return None + + + def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, + subtitle: Optional[str] = None, buttons: Optional[List] = None) -> Optional[Dict]: + """ + Sends a generic template message to the specified sender. + + Args: + sender_id (str): The ID of the recipient. + title (str): The title of the template. + image_url (Optional[str], optional): The URL of the image to be displayed. Defaults to None. + default_url (Optional[str], optional): The URL for the default action. Defaults to None. + subtitle (Optional[str], optional): The subtitle of the template. Defaults to None. + buttons (Optional[List], optional): A list of button options. Defaults to None. + + Returns: + Optional[Dict]: The response from the server if the request was successful, otherwise None. + """ - def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, subtitle: Optional[str] = None, buttons: Optional[list] = None): if default_url: default_action = { "type": "web_url", @@ -540,17 +576,37 @@ def send_generic_template(self, sender_id: str, title: str, image_url: Optional[ } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() - return r.json() - except requests.exceptions.RequestException as e: - print(f"Request failed: {e} \n{r.json()}") + logging.info("Response: %d - %s", 200, "Generic template sent successfully") + return jsonify(r.json(), 200) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) return None - def send_receipt_template(self, sender_id: str, order_number: str, payment_method: str, summary: dict, currency: str = 'USD', - order_url: Optional[str] = None, timestamp: Optional[str] = None, address: Optional[dict] = None, - adjustments: Optional[list] = None, elements: Optional[list] = None): + + def send_receipt_template(self, sender_id: str, order_number: str, payment_method: str, summary: Dict, currency: str = 'USD', + order_url: Optional[str] = None, timestamp: Optional[str] = None, address: Optional[Dict] = None, + adjustments: Optional[List] = None, elements: Optional[List] = None) -> Optional[Dict]: + """ + Sends a receipt template message to the specified sender. + + Args: + sender_id (str): The ID of the recipient. + order_number (str): The order number of the transaction. + payment_method (str): The payment method used. + summary (Dict): A dictionary containing the summary of the transaction. + currency (str, optional): The currency used in the transaction. Defaults to 'USD'. + order_url (Optional[str], optional): The URL of the order. Defaults to None. + timestamp (Optional[str], optional): The timestamp of the transaction. Defaults to None. + address (Optional[Dict], optional): The address of the recipient. Defaults to None. + adjustments (Optional[List], optional): A list of adjustments made to the order. Defaults to None. + elements (Optional[List], optional): A list of elements in the order. Defaults to None. + + Returns: + Optional[Dict]: The response from the server if the request was successful, otherwise None. + """ + header = {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"} body = { @@ -578,10 +634,10 @@ def send_receipt_template(self, sender_id: str, order_number: str, payment_metho } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() - return r.json() - except requests.exceptions.RequestException as e: - print(f"Request failed: {e} \n{r.json()}") + logging.info("Response: %d - %s", 200, "Receipt template sent successfully") + return jsonify(r.json(), 200) + except RequestException as e: + logging.info("Response: %d - %s", 403, e) return None From 8224fc9749231e7c6e55ab5677468c79b11814a2 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Mon, 24 Jun 2024 06:52:46 +0000 Subject: [PATCH 11/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/Messenger.py | 151 ++++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 77 deletions(-) diff --git a/pynani/Messenger.py b/pynani/Messenger.py index dc1475e..95a699d 100644 --- a/pynani/Messenger.py +++ b/pynani/Messenger.py @@ -13,15 +13,29 @@ from typing import Union, Optional, Tuple, Dict, List import requests from requests.exceptions import RequestException - - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s from Pynani - %(message)s' +from colorlog import ColoredFormatter + + +logger = logging.getLogger('Pynani') +logger.setLevel(logging.DEBUG) +formatter = ColoredFormatter( + "%(log_color)s%(levelname)s: %(name)s [%(asctime)s] -- %(message)s", + datefmt='%d/%m/%Y %H:%M:%S', + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'bold_red', + } ) +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) -def jsonify(data: Union[Dict, str], status_code: int) -> Tuple[Dict, int, Dict]: +def jsonify(data: Union[Dict, str], status_code: int) -> Tuple: """ Converts the given data to a JSON response with the specified status code. @@ -30,7 +44,7 @@ def jsonify(data: Union[Dict, str], status_code: int) -> Tuple[Dict, int, Dict]: status_code (int): The HTTP status code to be returned with the response. Returns: - Tuple[Dict, int, Dict]: A tuple containing the JSON response, the status code, and the headers. + Tuple: A tuple containing the JSON response, the status code, and the headers. """ if isinstance(data, dict): @@ -53,8 +67,7 @@ def __init__(self, access_token: str, page_id: str = 'me') -> None: self.page_id = page_id self.__url = f"https://graph.facebook.com/v20.0/{page_id}/messages" - - def verify_token(self, params: Dict, token: str) -> Tuple[Dict, int, Dict]: + def verify_token(self, params: Dict, token: str) -> Tuple: """ Verifies the provided token against the expected token. @@ -63,7 +76,7 @@ def verify_token(self, params: Dict, token: str) -> Tuple[Dict, int, Dict]: token (str): The expected verification token. Returns: - Tuple[Dict, int, Dict]: A tuple containing the JSON response, the status code, and the headers. + Tuple: A tuple containing the JSON response, the status code, and the headers. """ mode = params.get("hub.mode") @@ -72,16 +85,13 @@ def verify_token(self, params: Dict, token: str) -> Tuple[Dict, int, Dict]: if mode == "subscribe" and challenge: if hub_token != token: - logging.info('Response: %d - %s', 403, - 'Verification token mismatch') - return jsonify("Verification token mismatch", 403) - logging.info('Response: %d - %s', 200, "Verification successful") + logger.error('Verification token mismatch - %d', 403) + return jsonify({"Error": "Verification token mismatch"}, 403) + logger.info('Verification successful - %d', 200) return jsonify(challenge, 200) - logging.info('Response: %d - %s', 200, - "This endpoint is to verify token") + logger.warning('This endpoint is to verify token - %d', 200,) return jsonify(Path("pynani/verify_token.html").read_text(encoding='utf-8'), 200) - def get_sender_id(self, data: dict) -> Optional[str]: """ Extracts the sender ID from the provided data. @@ -96,10 +106,9 @@ def get_sender_id(self, data: dict) -> Optional[str]: try: return data['entry'][0]['messaging'][0]['sender']['id'] except (IndexError, KeyError) as e: - logging.info("Error accessing sender ID: %s", e) + logger.error("Error accessing sender ID: %s", e) return None - def get_message_type(self, data: Dict) -> Optional[str]: """ Determines the type of message received from the webhook event. @@ -130,11 +139,9 @@ def get_message_type(self, data: Dict) -> Optional[str]: else: return attachment_type except (IndexError, KeyError) as e: - logging.info("Error accessing message type: %s", e) - # print(f"Error accessing message type: {e}") + logger.error("Error accessing message type: %s", e) return None - def get_message_text(self, data: Dict) -> Optional[str]: """ Extracts the text message from the provided data. @@ -153,11 +160,9 @@ def get_message_text(self, data: Dict) -> Optional[str]: elif 'postback' in message: return message['postback']['title'] except (IndexError, KeyError) as e: - logging.info("Error accessing message text: %s", e) - # print(f"Error accessing message text: {e}") + logger.error("Error accessing message text: %s", e) return None - def send_text_message(self, sender_id: str, message: Union[str, int]) -> Optional[Dict]: """ Sends a text message to the specified sender. @@ -183,16 +188,15 @@ def send_text_message(self, sender_id: str, message: Union[str, int]) -> Optiona } try: - r = requests.post(self.__url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, + json=body, timeout=10) r.raise_for_status() - logging.info("Response: %d - %s", 200, "Message sent successfully") + logger.info("Message sent successfully - %d", 200) return jsonify(r.json(), 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) - # print(f"Request failed: {e} \n{r.json()}") + logger.error("%s - %d", e, 403) return None - def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: """ Uploads an attachment to the server and returns the attachment ID. @@ -205,7 +209,8 @@ def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: str: The ID of the uploaded attachment if successful, otherwise None. """ - attachments_url = f"https://graph.facebook.com/v20.0/{self.page_id}/message_attachments" + attachments_url = f"https://graph.facebook.com/v20.0/{ + self.page_id}/message_attachments" attachment = Path(attachment_path) mimetype, _ = mimetypes.guess_type(attachment) @@ -229,16 +234,13 @@ def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: r = requests.post(attachments_url, headers=header, files=file, data=body, timeout=20) r.raise_for_status() - logging.info("Response: %d - %s", 200, - "Attachment uploaded successfully") + logger.info("Attachment uploaded successfully - %d", 200) attachment_id = r.json()["attachment_id"] return attachment_id except (RequestException, IndexError, KeyError) as e: - logging.info("Response: %d - %s", 403, e) - # print(f"Request failed: {e} \n{r.json()}") + logger.error("%s - %d", e, 403) return None - def get_url_attachment(self, data: Dict) -> Optional[str]: """ Extracts the URL of an attachment from the provided data. @@ -253,11 +255,9 @@ def get_url_attachment(self, data: Dict) -> Optional[str]: try: return data['entry'][0]['messaging'][0]['message']['attachments'][0]["payload"]["url"] except (IndexError, KeyError) as e: - logging.info("Error accessing attachment url: %s", e) - # print(f"Error accessing attachment url: {e}") + logger.error("Error accessing attachment url: %s", e) return None - def get_attachment_type(self, data: Dict) -> Optional[str]: """ Extracts the type of an attachment from the provided data. @@ -272,11 +272,9 @@ def get_attachment_type(self, data: Dict) -> Optional[str]: try: return data['entry'][0]['messaging'][0]['message']['attachments'][0]["type"] except (IndexError, KeyError) as e: - logging.info("Error accessing attachment type: %s", e) - # print(f"Error accessing attachment type: {e}") + logger.error("Error accessing attachment type: %s", e) return None - def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: str) -> Optional[Dict]: """ Sends an attachment to a user. @@ -309,16 +307,15 @@ def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: } try: - r = requests.post(self.__url, headers=header, json=body, timeout=15) + r = requests.post(self.__url, headers=header, + json=body, timeout=15) r.raise_for_status() - logging.info("Response: %d - %s", 200, "Attachment sent successfully") + logger.info("Attachment sent successfully - %d", 200) return jsonify(r.json(), 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) - # print(f"Request failed: {e} \n{r.json()}") + logger.error("%s - %d", e, 403) return None - def send_local_attachment(self, sender_id: str, attachment_type: str, attachment_path: str) -> Optional[Dict]: """ Sends a local attachment to a user. @@ -355,15 +352,14 @@ def send_local_attachment(self, sender_id: str, attachment_type: str, attachment } try: - r = requests.post(self.__url, headers=header, data=body, files=file, timeout=15) + r = requests.post(self.__url, headers=header, + data=body, files=file, timeout=15) r.raise_for_status() - logging.info("Response: %d - %s", 200, "Attachment sent successfully") + logger.info("Attachment sent successfully - %d", 200) return jsonify(r.json(), 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) - # print(f"Request failed: {e} \n{r.json()}") + logger.error("%s - %d", e, 403) return None - def download_attachment(self, attachment_url: str, path_dest: str) -> None: """ @@ -383,12 +379,12 @@ def download_attachment(self, attachment_url: str, path_dest: str) -> None: with open(path_dest, 'wb') as file: for chunk in r.iter_content(1024): file.write(chunk) - logging.info("Response: %d - Downloaded attachment successfully to %s", 200, path_dest) + logger.info( + "Downloaded attachment successfully to \"%s\" - %d", path_dest, 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) + logger.error("%s - %d", e, 403) return None - def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_replies: List[Dict]) -> Optional[Dict]: """ Sends a quick reply message to the specified sender. @@ -403,8 +399,7 @@ def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_repli """ if len(quick_replies) > 13: - logging.info("Quick replies should be less than 13") - # print("Quick replies should be less than 13") + logger.warning("Quick replies should be less than 13") quick_replies = quick_replies[:13] header = {"Content-Type": "application/json", @@ -421,12 +416,13 @@ def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_repli } try: - r = requests.post(self.__url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, + json=body, timeout=10) r.raise_for_status() - logging.info("Response: %d - %s", 200, "Quick reply sent successfully") + logger.info("Quick reply sent successfully - %d", 200) return jsonify(r.json(), 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) + logger.error("%s - %d", e, 403) return None def send_button_template(self, sender_id: str, message: str, buttons: List[Dict]) -> Optional[Dict]: @@ -443,7 +439,7 @@ def send_button_template(self, sender_id: str, message: str, buttons: List[Dict] """ if len(buttons) > 3: - logging.info("Buttons template should be less than 3") + logger.warning("Buttons template should be less than 3") buttons = buttons[:3] header = {"Content-Type": "application/json", @@ -466,15 +462,15 @@ def send_button_template(self, sender_id: str, message: str, buttons: List[Dict] } try: - r = requests.post(self.__url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, + json=body, timeout=10) r.raise_for_status() - logging.info("Response: %d - %s", 200, "Button template sent successfully") + logger.info("Button template sent successfully - %d", 200) return jsonify(r.json(), 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) + logger.error("%s - %d", e, 403) return None - def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: List[Dict]) -> Optional[Dict]: """ Sends a media template message to the specified sender. @@ -514,16 +510,16 @@ def send_media_template(self, sender_id: str, media_type: str, attachment_id: st } try: - r = requests.post(self.__url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, + json=body, timeout=10) r.raise_for_status() - logging.info("Response: %d - %s", 200, "Media template sent successfully") + logger.info("Media template sent successfully - %d", 200) return jsonify(r.json(), 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) + logger.error("%s - %d", e, 403) return None - - def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, + def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, subtitle: Optional[str] = None, buttons: Optional[List] = None) -> Optional[Dict]: """ Sends a generic template message to the specified sender. @@ -576,15 +572,15 @@ def send_generic_template(self, sender_id: str, title: str, image_url: Optional[ } try: - r = requests.post(self.__url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, + json=body, timeout=10) r.raise_for_status() - logging.info("Response: %d - %s", 200, "Generic template sent successfully") + logger.info("Generic template sent successfully - %d", 200) return jsonify(r.json(), 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) + logger.error("%s - %d", e, 403) return None - def send_receipt_template(self, sender_id: str, order_number: str, payment_method: str, summary: Dict, currency: str = 'USD', order_url: Optional[str] = None, timestamp: Optional[str] = None, address: Optional[Dict] = None, adjustments: Optional[List] = None, elements: Optional[List] = None) -> Optional[Dict]: @@ -634,10 +630,11 @@ def send_receipt_template(self, sender_id: str, order_number: str, payment_metho } try: - r = requests.post(self.__url, headers=header, json=body, timeout=10) + r = requests.post(self.__url, headers=header, + json=body, timeout=10) r.raise_for_status() - logging.info("Response: %d - %s", 200, "Receipt template sent successfully") + logger.info("Receipt template sent successfully - %d", 200) return jsonify(r.json(), 200) except RequestException as e: - logging.info("Response: %d - %s", 403, e) + logger.error("%s - %d", e, 403) return None From 50433f54942f9fea9ae68c91324bb64aa486cac2 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Mon, 24 Jun 2024 16:42:22 +0000 Subject: [PATCH 12/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/utils/Buttons.py | 146 ++++++++++++++++++++++++------------ pynani/utils/__init__.py | 1 + pynani/utils/logs.py | 25 ++++++ pynani/utils/quick_reply.py | 90 ++++++++++++++++++++++ pynani/utils/receipt.py | 126 +++++++++++++++++++++++++++++++ 5 files changed, 341 insertions(+), 47 deletions(-) create mode 100644 pynani/utils/__init__.py create mode 100644 pynani/utils/logs.py create mode 100644 pynani/utils/quick_reply.py create mode 100644 pynani/utils/receipt.py diff --git a/pynani/utils/Buttons.py b/pynani/utils/Buttons.py index a209955..60f31a5 100644 --- a/pynani/utils/Buttons.py +++ b/pynani/utils/Buttons.py @@ -1,52 +1,104 @@ -from typing import Optional, Union - - -class Buttons: - def __make_button(self, title: str, url: Optional[str] = None, call_number: Optional[str] = None) -> dict: - if url is not None and call_number is not None: - raise ValueError("You can't have both url and call_number at the same time") - - if url: - type_button = "web_url" - url_button = url - payload_button = "" - elif call_number: - type_button = "phone_number" - url_button = "" - payload_button = call_number - elif not url and not call_number: - type_button = "postback" - url_button = "" - payload_button = "DEVELOPER_DEFINED_PAYLOAD" - - return { - "type": type_button, - "title": title, - "payload": payload_button, - "url": url_button, - } +from typing import Optional, Union, List, Dict +from .logs import logger + + +def __make_button( title: str, url: Optional[str] = None, call_number: Optional[str] = None) -> Dict: + """ + Creates a button with the specified title and optional URL or call number. + + Args: + title (str): The title of the button. + url (Optional[str], optional): The URL associated with the button. Defaults to None. + call_number (Optional[str], optional): The call number associated with the button. Defaults to None. + + Returns: + Dict: A dictionary representing the button with its properties. + + Raises: + ValueError: If both URL and call number are provided. + + Example: + >>> __make_button("Hola", "https://www.google.com") + {'type': 'web_url', 'title': 'Hola', 'payload': '', 'url': 'https://www.google.com'} + >>> __make_button("Mundo", call_number="+525555555555") + {'type': 'phone_number', 'title': 'Mundo', 'payload': '+525555555555', 'url': ''} + >>> __make_button("Hello") + {'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''} + """ + + if url is not None and call_number is not None: + logger.error("You can't have both url and call_number at the same time") + raise ValueError("You can't have both url and call_number at the same time") - def basic_buttons(self, buttons: Union[str, list]) -> list: - if isinstance(buttons, str): - return [self.__make_button(buttons)] - else: - if len(buttons) > 3: - print("Buttons template should be less than 3") - buttons = buttons[:3] - return [self.__make_button(button) for button in buttons] + if url: + type_button = "web_url" + url_button = url + payload_button = "" + elif call_number: + type_button = "phone_number" + url_button = "" + payload_button = call_number + elif not url and not call_number: + type_button = "postback" + url_button = "" + payload_button = "DEVELOPER_DEFINED_PAYLOAD" - def leave_buttons(self, buttons: Union[dict, list]): - if isinstance(buttons, dict): - return [self.__make_button(**buttons)] - else: - if len(buttons) > 3: - print("Buttons template should be less than 3") - buttons = buttons[:3] - return [self.__make_button(**button) for button in buttons] + return { + "type": type_button, + "title": title, + "payload": payload_button, + "url": url_button, + } - -# bb = Button() -# # print(bb.basic_buttons(["Hola", "Mundo", "馃敟"])) -# print(bb.leave_buttons([{"title": "Hola", "url": "https://www.google.com"}, {"title": "Mundo", "call_number": "+525555555555"}])) +def basic_buttons( buttons: Union[str, List[Union[str, int]]]) -> List: + """ + Creates a list of basic buttons. + Args: + buttons (Union[str, List[Union[str, int]]]): The buttons to be created. It can be a string or a list of strings or integers. + + Returns: + List: A list of dictionaries representing the basic buttons with their properties. + + Example: + >>> basic_buttons(["Hello", "World", "馃敟"]) + [{'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}, + {'type': 'postback', 'title': 'World', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}, + {'type': 'postback', 'title': '馃敟', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}] + >>> basic_buttons("Hello") + [{'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}] + """ + + if isinstance(buttons, str): + return [__make_button(buttons)] + else: + if len(buttons) > 3: + logger.warning("Buttons template should be less than 3") + buttons = buttons[:3] + return [__make_button(button) for button in buttons] + +def leave_buttons( buttons: Union[Dict, List[Dict]]) -> List: + """ + Creates a list of leave buttons. + + Args: + buttons (Union[Dict, List[Dict]]): The buttons to be created. It can be a dictionary or a list of dictionaries. + + Returns: + List: A list of dictionaries representing the leave buttons with their properties. + + Example: + >>> leave_buttons([{"title": "Hello", "url": "https://www.google.com"}, {"title": "World", "call_number": "+525555555555"}]) + [{'type': 'web_url', 'title': 'Hello', 'payload': '', 'url': 'https://www.google.com'}, + {'type': 'phone_number', 'title': 'World', 'payload': '+525555555555', 'url': ''}] + >>> leave_buttons({"title": "Hello", "url": "https://www.google.com"}) + [{'type': 'web_url', 'title': 'Hello', 'payload': '', 'url': 'https://www.google.com'}] + """ + if isinstance(buttons, dict): + return [__make_button(**buttons)] + else: + if len(buttons) > 3: + logger.warning("Buttons template should be less than 3") + buttons = buttons[:3] + return [__make_button(**button) for button in buttons] diff --git a/pynani/utils/__init__.py b/pynani/utils/__init__.py new file mode 100644 index 0000000..c1c2a87 --- /dev/null +++ b/pynani/utils/__init__.py @@ -0,0 +1 @@ +from .logs import logger \ No newline at end of file diff --git a/pynani/utils/logs.py b/pynani/utils/logs.py new file mode 100644 index 0000000..3749bcb --- /dev/null +++ b/pynani/utils/logs.py @@ -0,0 +1,25 @@ +"""This file is used to configure the logger for the project""" + + +import logging +from colorlog import ColoredFormatter + + +logger = logging.getLogger('Pynani') +logger.setLevel(logging.DEBUG) +formatter = ColoredFormatter( + "%(log_color)s%(levelname)s: %(name)s [%(asctime)s] -- %(message)s", + datefmt='%d/%m/%Y %H:%M:%S', + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'bold_red', + } +) +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) + +logger.addHandler(console_handler) diff --git a/pynani/utils/quick_reply.py b/pynani/utils/quick_reply.py new file mode 100644 index 0000000..59eb105 --- /dev/null +++ b/pynani/utils/quick_reply.py @@ -0,0 +1,90 @@ +from typing import Optional, Union, List, Dict +from .logs import logger + + +def __make_quick_button(text: Union[str, int], image_url: Optional[str] = None) -> Dict: + """ + Creates a quick reply button with the specified text, optional image URL, and payload. + + Args: + text (Union[str, int]): The text or integer to be displayed on the button. + image_url (Optional[str], optional): The URL of the image to be displayed on the button. Defaults to None. + + Returns: + Dict: A dictionary representing the quick reply button with its properties. + + Example: + >>> __make_quick_button("Hello") + {'content_type': 'text', 'title': 'Hello', 'payload': '', 'image_url': ''} + >>> __make_quick_button("World", "https://photos.com/world.jpg") + {'content_type': 'text', 'title': 'World', 'payload': '', 'image_url': 'https://photos.com/world.jpg'} + """ + + return { + "content_type": "text", + "title": text, + "payload": "", + "image_url": image_url, + } + +def quick_buttons(buttons: List[Union[str, int]]) -> List[Dict]: + """ + Prepares a list of quick reply buttons from a list of strings. + + Args: + buttons (List[Union[str, int]]): A list of strings or integers representing the text for each quick reply button. + + Returns: + List: A list of dictionaries representing the quick reply buttons with their properties. + + Example: + >>> quick_buttons(["Hello", "World"]) + [{'content_type': 'text', 'title': 'Hello', 'payload': '', 'image_url': ''}, + {'content_type': 'text', 'title': 'World', 'payload': '', 'image_url': ''}] + >>> quick_buttons([1, 2, 3]) + [{'content_type': 'text', 'title': '1', 'payload': '', 'image_url': ''}, + {'content_type': 'text', 'title': '2', 'payload': '', 'image_url': ''}, + {'content_type': 'text', 'title': '3', 'payload': '', 'image_url': ''}] + """ + + r_buttons = [] + if len(buttons) > 13: + logger.warning("Quick replies should be less than 13") + buttons = buttons[:13] + + for b in buttons: + r_buttons.append(__make_quick_button(b)) + + return r_buttons + +def quick_buttons_image(buttons: List) -> List[Dict]: + """ + Prepares a list of quick reply buttons with images from a list of dictionaries. + + Args: + buttons (List): A list of dictionaries representing the quick reply buttons with their properties. + + Returns: + List: A list of dictionaries representing the quick reply buttons with their properties, including images. + + Raises: + ValueError: If each button is not a dictionary. + + Example: + >>> quick_buttons_image([{"text": "Hello", "image_url": "https://photos.com/hello.jpg"}, {"text": "World", "image_url": "https://photos.com/world.jpg"}]) + [{'content_type': 'text', 'title': 'Hello', 'payload': '', 'image_url': 'https://photos.com/hello.jpg'}, + {'content_type': 'text', 'title': 'World', 'payload': '', 'image_url': 'https://photos.com/world.jpg'}] + """ + + r_buttons = [] + if len(buttons) > 13: + logger.warning("Quick replies should be less than 13") + buttons = buttons[:13] + for b in buttons: + if not isinstance(b, dict): + logger.error("Each button should be a dictionary") + raise ValueError("Each button should be a dictionary") + else: + r_buttons.append(__make_quick_button(**b)) + + return r_buttons diff --git a/pynani/utils/receipt.py b/pynani/utils/receipt.py new file mode 100644 index 0000000..7aa415b --- /dev/null +++ b/pynani/utils/receipt.py @@ -0,0 +1,126 @@ +from typing import Optional, Union, List, Dict + + +def get_address(street_1: str, city: str, postal_code: str, state: str, + country: str, street_2: Optional[str] = None) -> Dict: + """ + Constructs an address dictionary from the provided parameters. + + Args: + street_1 (str): The first line of the street address. + city (str): The city of the address. + postal_code (str): The postal code of the address. + state (str): The state or province of the address. + country (str): The country of the address. + street_2 (Optional[str], optional): The second line of the street address. Defaults to None. + + Returns: + Dict: A dictionary representing the address with its components. + + Example: + >>> get_address("123 Main St", "Springfield", "12345", "IL", "US", "Apt 1") + {'street_1': '123 Main St', 'street_2': "Apt 1", 'city': 'Springfield', 'postal_code': '12345', 'state': 'IL', 'country': 'US'} + Optional parameters can be omitted: + >>> get_address("123 Main St", "Springfield", "12345", "IL", "US") + {'street_1': '123 Main St', 'street_2': None, 'city': 'Springfield', 'postal_code': '12345', 'state': 'IL', 'country': 'US'} + """ + + return { + "street_1": street_1, + "street_2": street_2, + "city": city, + "postal_code": postal_code, + "state": state, + "country": country + } + + +def get_summary(total_cost: float, subtotal: Optional[float] = None, + shipping_cost: Optional[float] = None, total_tax: Optional[float] = None) -> Dict: + """ + Constructs a summary dictionary from the provided parameters. + + Args: + total_cost (float): The total cost of the order. + subtotal (Optional[float], optional): The subtotal of the order. Defaults to None. + shipping_cost (Optional[float], optional): The shipping cost of the order. Defaults to None. + total_tax (Optional[float], optional): The total tax of the order. Defaults to None. + + Returns: + Dict: A dictionary representing the summary with its components. + + Example: + >>> get_summary(100.0, 90.0, 5.0, 5.0) + {'subtotal': 90.0, 'shipping_cost': 5.0, 'total_tax': 5.0, 'total_cost': 100.0} + Optional parameters can be omitted: + >>> get_summary(100.0) + {'subtotal': None, 'shipping_cost': None, 'total_tax': None, 'total_cost': 100.0} + """ + + return { + "subtotal": subtotal, + "shipping_cost": shipping_cost, + "total_tax": total_tax, + "total_cost": total_cost + } + + +def get_adjustments(*args: Union[str, int, float]) -> List[Dict]: + """ + Constructs a list of adjustments from the provided arguments. + + Args: + *args (Union[str, int, float]): A variable number of arguments, where each pair represents an adjustment name and amount. + + Returns: + List[Dict]: A list of dictionaries, each representing an adjustment with its name and amount. + + Example: + >>> get_adjustments("Discount", 10, "Tax", 20, "Shipping", 30) + [{'name': 'Discount', 'amount': 10}, {'name': 'Tax', 'amount': 20}, {'name': 'Shipping', 'amount': 30}] + """ + + adjustments = [] + for i in range(0, len(args), 2): + adjustment = { + "name": args[i], + "amount": args[i + 1] + } + adjustments.append(adjustment) + return adjustments + + +def get_elements(title: str, price: float, subtitle: Optional[str] = None, + quantity: Optional[int] = None, currency: Optional[str] = 'USD', + image_url: Optional[str] = None) -> List[Dict]: + """ + Constructs a list of elements from the provided parameters. + + Args: + title (str): The title of the item. + price (float): The price of the item. + subtitle (Optional[str], optional): The subtitle of the item. Defaults to None. + quantity (Optional[int], optional): The quantity of the item. Defaults to None. + currency (Optional[str], optional): The currency of the item. Defaults to 'USD'. + image_url (Optional[str], optional): The URL of the item's image. Defaults to None. + + Returns: + List: A list of dictionaries representing the elements with their titles, prices, subtitles, quantities, currencies, and image URLs. + + Example: + >>> get_elements("T-Shirt", 20.0, "Blue", 2, "USD", "https://example.com/tshirt.jpg") + [{'title': 'T-Shirt', 'subtitle': 'Blue', 'quantity': 2, 'price': 20.0, 'currency': 'USD', 'image_url': 'https://example.com/tshirt.jpg'}] + Optional parameters can be omitted: + >>> get_elements("T-Shirt", 20.0) + [{'title': 'T-Shirt', 'subtitle': None, 'quantity': None, 'price': 20.0, 'currency': 'USD', 'image_url': None}] + """ + + item = { + "title": title, + "subtitle": subtitle, + "quantity": quantity, + "price": price, + "currency": currency, + "image_url": image_url + } + return [item] From c1014e0ddbe6fd8f53379005426e097bf0b4573c Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Mon, 24 Jun 2024 22:47:44 +0000 Subject: [PATCH 13/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/Messenger.py | 167 +++++++++++++++++++++++++++--------- pynani/__init__.py | 6 +- pynani/utils/Buttons.py | 2 +- pynani/utils/quick_reply.py | 35 ++++---- 4 files changed, 146 insertions(+), 64 deletions(-) diff --git a/pynani/Messenger.py b/pynani/Messenger.py index 95a699d..5e069c5 100644 --- a/pynani/Messenger.py +++ b/pynani/Messenger.py @@ -8,31 +8,11 @@ import mimetypes import json -import logging from pathlib import Path from typing import Union, Optional, Tuple, Dict, List import requests from requests.exceptions import RequestException -from colorlog import ColoredFormatter - - -logger = logging.getLogger('Pynani') -logger.setLevel(logging.DEBUG) -formatter = ColoredFormatter( - "%(log_color)s%(levelname)s: %(name)s [%(asctime)s] -- %(message)s", - datefmt='%d/%m/%Y %H:%M:%S', - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'bold_red', - } -) -console_handler = logging.StreamHandler() -console_handler.setLevel(logging.DEBUG) -console_handler.setFormatter(formatter) -logger.addHandler(console_handler) +from .utils import logger def jsonify(data: Union[Dict, str], status_code: int) -> Tuple: @@ -67,6 +47,7 @@ def __init__(self, access_token: str, page_id: str = 'me') -> None: self.page_id = page_id self.__url = f"https://graph.facebook.com/v20.0/{page_id}/messages" + def verify_token(self, params: Dict, token: str) -> Tuple: """ Verifies the provided token against the expected token. @@ -77,6 +58,13 @@ def verify_token(self, params: Dict, token: str) -> Tuple: Returns: Tuple: A tuple containing the JSON response, the status code, and the headers. + + Example: + >>> verify_token({"hub.mode": "subscribe", "hub.challenge": "1950414725", "hub.verify_token": "1234", }, "1234") + Whit a Webhook made with Flask, the function verify_token() will be used as follows: + >>> @app.get("/") + >>> def meta_verify(): + >>> return mess.verify_token(request.args, TOKEN) """ mode = params.get("hub.mode") @@ -92,6 +80,7 @@ def verify_token(self, params: Dict, token: str) -> Tuple: logger.warning('This endpoint is to verify token - %d', 200,) return jsonify(Path("pynani/verify_token.html").read_text(encoding='utf-8'), 200) + def get_sender_id(self, data: dict) -> Optional[str]: """ Extracts the sender ID from the provided data. @@ -101,6 +90,10 @@ def get_sender_id(self, data: dict) -> Optional[str]: Returns: Optional[str]: The sender ID if found, otherwise None. + + Example with a Webhook made with Flask, will be used as follows: + >>> get_sender_id(request.get_json()) + "1234567897654321" """ try: @@ -109,6 +102,7 @@ def get_sender_id(self, data: dict) -> Optional[str]: logger.error("Error accessing sender ID: %s", e) return None + def get_message_type(self, data: Dict) -> Optional[str]: """ Determines the type of message received from the webhook event. @@ -118,6 +112,10 @@ def get_message_type(self, data: Dict) -> Optional[str]: Returns: Optional[str]: The type of message if found, otherwise None. + + Example with a Webhook made with Flask, will be used as follows: + >>> get_message_type(request.get_json()) + "text" """ messaging = data['entry'][0]['messaging'][0] @@ -142,6 +140,7 @@ def get_message_type(self, data: Dict) -> Optional[str]: logger.error("Error accessing message type: %s", e) return None + def get_message_text(self, data: Dict) -> Optional[str]: """ Extracts the text message from the provided data. @@ -151,6 +150,10 @@ def get_message_text(self, data: Dict) -> Optional[str]: Returns: Optional[str]: The text message if found, otherwise None. + + Example with a Webhook made with Flask, will be used as follows: + >>> get_message_text(request.get_json()) + "Hello 馃憢馃徑" """ try: @@ -163,6 +166,7 @@ def get_message_text(self, data: Dict) -> Optional[str]: logger.error("Error accessing message text: %s", e) return None + def send_text_message(self, sender_id: str, message: Union[str, int]) -> Optional[Dict]: """ Sends a text message to the specified sender. @@ -173,6 +177,9 @@ def send_text_message(self, sender_id: str, message: Union[str, int]) -> Optiona Returns: Optional[Dict]: The response from the server if the request was successful, otherwise None. + + Example with a Webhook made with Flask, will be used as follows: + >>> send_text_message(sender_id, "Hello, how can I help you?") """ header = {"Content-Type": "application/json", @@ -188,8 +195,7 @@ def send_text_message(self, sender_id: str, message: Union[str, int]) -> Optiona } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() logger.info("Message sent successfully - %d", 200) return jsonify(r.json(), 200) @@ -197,6 +203,7 @@ def send_text_message(self, sender_id: str, message: Union[str, int]) -> Optiona logger.error("%s - %d", e, 403) return None + def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: """ Uploads an attachment to the server and returns the attachment ID. @@ -207,6 +214,10 @@ def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: Returns: str: The ID of the uploaded attachment if successful, otherwise None. + + Example with a Webhook made with Flask, will be used as follows: + >>> upload_attachment("image", "path/to/image.png") + "1234567897654321" """ attachments_url = f"https://graph.facebook.com/v20.0/{ @@ -231,8 +242,7 @@ def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: body = {"message": str(message)} try: - r = requests.post(attachments_url, headers=header, - files=file, data=body, timeout=20) + r = requests.post(attachments_url, headers=header, files=file, data=body, timeout=20) r.raise_for_status() logger.info("Attachment uploaded successfully - %d", 200) attachment_id = r.json()["attachment_id"] @@ -241,6 +251,7 @@ def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: logger.error("%s - %d", e, 403) return None + def get_url_attachment(self, data: Dict) -> Optional[str]: """ Extracts the URL of an attachment from the provided data. @@ -250,6 +261,10 @@ def get_url_attachment(self, data: Dict) -> Optional[str]: Returns: Optional[str]: The URL of the attachment if found, otherwise None. + + Example with a Webhook made with Flask, will be used as follows: + >>> get_url_attachment(request.get_json()) + "https://example.com/image.png" """ try: @@ -258,6 +273,7 @@ def get_url_attachment(self, data: Dict) -> Optional[str]: logger.error("Error accessing attachment url: %s", e) return None + def get_attachment_type(self, data: Dict) -> Optional[str]: """ Extracts the type of an attachment from the provided data. @@ -267,6 +283,10 @@ def get_attachment_type(self, data: Dict) -> Optional[str]: Returns: Optional[str]: The type of the attachment if found, otherwise None. + + Example with a Webhook made with Flask, will be used as follows: + >>> get_attachment_type(request.get_json()) + "image" """ try: @@ -275,6 +295,7 @@ def get_attachment_type(self, data: Dict) -> Optional[str]: logger.error("Error accessing attachment type: %s", e) return None + def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: str) -> Optional[Dict]: """ Sends an attachment to a user. @@ -286,6 +307,9 @@ def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: Returns: Optional[Dict]: The response from the server if the request is successful, otherwise None. + + Example: + >>> send_attachment(sender_id, "image", "https://example.com/image.png") """ header = {"Content-Type": "application/json", @@ -307,8 +331,7 @@ def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=15) + r = requests.post(self.__url, headers=header, json=body, timeout=15) r.raise_for_status() logger.info("Attachment sent successfully - %d", 200) return jsonify(r.json(), 200) @@ -316,6 +339,7 @@ def send_attachment(self, sender_id: str, attachment_type: str, attachment_url: logger.error("%s - %d", e, 403) return None + def send_local_attachment(self, sender_id: str, attachment_type: str, attachment_path: str) -> Optional[Dict]: """ Sends a local attachment to a user. @@ -327,6 +351,9 @@ def send_local_attachment(self, sender_id: str, attachment_type: str, attachment Returns: Optional[Dict]: The response from the server if the request is successful, otherwise None. + + Example: + >>> send_local_attachment(sender_id, "image", "path/to/image.png") """ attachment = Path(attachment_path) @@ -352,8 +379,7 @@ def send_local_attachment(self, sender_id: str, attachment_type: str, attachment } try: - r = requests.post(self.__url, headers=header, - data=body, files=file, timeout=15) + r = requests.post(self.__url, headers=header, data=body, files=file, timeout=15) r.raise_for_status() logger.info("Attachment sent successfully - %d", 200) return jsonify(r.json(), 200) @@ -361,6 +387,7 @@ def send_local_attachment(self, sender_id: str, attachment_type: str, attachment logger.error("%s - %d", e, 403) return None + def download_attachment(self, attachment_url: str, path_dest: str) -> None: """ Downloads an attachment from the given URL and saves it to the specified destination path. @@ -371,6 +398,9 @@ def download_attachment(self, attachment_url: str, path_dest: str) -> None: Returns: None + + Example: + >>> download_attachment("https://example.com/image.png", "path/to/image.png") """ try: @@ -379,11 +409,11 @@ def download_attachment(self, attachment_url: str, path_dest: str) -> None: with open(path_dest, 'wb') as file: for chunk in r.iter_content(1024): file.write(chunk) - logger.info( - "Downloaded attachment successfully to \"%s\" - %d", path_dest, 200) + logger.info("Downloaded attachment successfully to \"%s\" - %d", path_dest, 200) except RequestException as e: logger.error("%s - %d", e, 403) return None + def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_replies: List[Dict]) -> Optional[Dict]: """ @@ -396,6 +426,13 @@ def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_repli Returns: Optional[Dict]: The response from the server if the request was successful, otherwise None. + + Example: + >>> send_quick_reply(sender_id, "Select an option", [{'content_type': 'text', 'title': 'Hello', 'payload': '', 'image_url': None}]) + Using the quick_buttons() function from pynani: + >>> send_quick_reply(sender_id, "Select an option", quick_buttons(["Hello", "World", "馃挬"])) + Usiing the quick_buttons_image() function from pynani: + >>> send_quick_reply(sender_id, "Select an option", quick_buttons_image(["Hello", "World", "馃挬"], ["https://example.com/hello.png", "https://example.com/world.png", "https://example.com/poop.png"])) """ if len(quick_replies) > 13: @@ -416,8 +453,7 @@ def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_repli } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() logger.info("Quick reply sent successfully - %d", 200) return jsonify(r.json(), 200) @@ -425,6 +461,7 @@ def send_quick_reply(self, sender_id: str, message: Union[str, int], quick_repli logger.error("%s - %d", e, 403) return None + def send_button_template(self, sender_id: str, message: str, buttons: List[Dict]) -> Optional[Dict]: """ Sends a button template message to the specified sender. @@ -436,6 +473,13 @@ def send_button_template(self, sender_id: str, message: str, buttons: List[Dict] Returns: Optional[Dict]: The response from the server if the request was successful, otherwise None. + + Example: + >>> send_button_template(sender_id, "Select an option", [{'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}]) + Using the basic_buttons() function from pynani: + >>> send_button_template(sender_id, "Select an option", basic_buttons(["Hello", "World", "馃"])) + Using the exit_buttons() function from pynani: + >>> send_button_template(sender_id, "Select an option", exit_buttons([{"title": "Exit", "url": "https://google.com"}, {"title": "Call me", "call_number": "+525555555555"}])) """ if len(buttons) > 3: @@ -462,8 +506,7 @@ def send_button_template(self, sender_id: str, message: str, buttons: List[Dict] } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() logger.info("Button template sent successfully - %d", 200) return jsonify(r.json(), 200) @@ -471,6 +514,7 @@ def send_button_template(self, sender_id: str, message: str, buttons: List[Dict] logger.error("%s - %d", e, 403) return None + def send_media_template(self, sender_id: str, media_type: str, attachment_id: str, buttons: List[Dict]) -> Optional[Dict]: """ Sends a media template message to the specified sender. @@ -483,6 +527,11 @@ def send_media_template(self, sender_id: str, media_type: str, attachment_id: st Returns: Optional[Dict]: The response from the server if the request was successful, otherwise None. + + Example: + >>> send_media_template(sender_id, "image", "1234567897654321", [{'type': 'web_url', 'title': 'Hello', 'payload': '', 'url': 'https://example.com/hello.png'}]) + Using the basic_buttons() or exit_buttons() functions from pynani: + >>> send_media_template(sender_id, "image", "1234567897654321", basic_buttons(["Hello", "World", "馃懟"])) """ header = {"Content-Type": "application/json", @@ -510,8 +559,7 @@ def send_media_template(self, sender_id: str, media_type: str, attachment_id: st } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() logger.info("Media template sent successfully - %d", 200) return jsonify(r.json(), 200) @@ -519,6 +567,7 @@ def send_media_template(self, sender_id: str, media_type: str, attachment_id: st logger.error("%s - %d", e, 403) return None + def send_generic_template(self, sender_id: str, title: str, image_url: Optional[str] = None, default_url: Optional[str] = None, subtitle: Optional[str] = None, buttons: Optional[List] = None) -> Optional[Dict]: """ @@ -534,6 +583,11 @@ def send_generic_template(self, sender_id: str, title: str, image_url: Optional[ Returns: Optional[Dict]: The response from the server if the request was successful, otherwise None. + + Example: + >>> send_generic_template(sender_id, "Hello", "https://example.com/hello.png", "https://example.com", "World", [{'type': 'web_url', 'title': 'Hello', 'payload': '', 'url': 'https://example.com/hello.png'}]) + Using the basic_buttons() or exit_buttons() functions from pynani: + >>> send_generic_template(sender_id, "Hello", "https://example.com/hello.png", "https://example.com", "World", basic_buttons(["Hello", "World", "馃懡"])) """ if default_url: @@ -572,8 +626,7 @@ def send_generic_template(self, sender_id: str, title: str, image_url: Optional[ } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() logger.info("Generic template sent successfully - %d", 200) return jsonify(r.json(), 200) @@ -581,6 +634,7 @@ def send_generic_template(self, sender_id: str, title: str, image_url: Optional[ logger.error("%s - %d", e, 403) return None + def send_receipt_template(self, sender_id: str, order_number: str, payment_method: str, summary: Dict, currency: str = 'USD', order_url: Optional[str] = None, timestamp: Optional[str] = None, address: Optional[Dict] = None, adjustments: Optional[List] = None, elements: Optional[List] = None) -> Optional[Dict]: @@ -601,6 +655,38 @@ def send_receipt_template(self, sender_id: str, order_number: str, payment_metho Returns: Optional[Dict]: The response from the server if the request was successful, otherwise None. + + Example: + >>> send_receipt_template(sender_id, "123456789", "Credit Card", + {"subtotal": 75.00, + "shipping_cost": 4.95, + "total_tax": 6.19, + "total_cost": 56.14}, + "USD", "https://example.com/order/123456789", "123456789", + {"street_1": "1 Hacker Way", + "city": "Menlo Park", + "postal_code": "94025", + "state": "CA", + "country": "US"}, + [{"name": "New Customer Discount", "amount": 20}], + [{"title": "Classic White T-Shirt", + "subtitle": "100% Soft and Luxurious Cotton", + "quantity": 2, "price": 50, + "currency": "USD", + "image_url": "https://example.com/classic-white-t-shirt"}]) + Using the get_address(), get_summary(), get_adjustments(), and get_elements() functions from pynani: + >>> address = get_address("123 Main St", "Springfield", "12345", "IL", "US") + >>> adjustments = get_adjustments("New Customer Discount", 20, "Black Friday", 34) + >>> summary = get_summary(56.14) + >>> elements = get_elements("T-Shirt", 20.0) + >>> send_receipt_template(sender_id, "123456789", "Credit Card", + summary=summary, + currency="USD", + order_url="https://example.com/order/123456789", + timestamp="123456789", + address=address, + adjustments=adjustments, + elements=elements) """ header = {"Content-Type": "application/json", @@ -630,8 +716,7 @@ def send_receipt_template(self, sender_id: str, order_number: str, payment_metho } try: - r = requests.post(self.__url, headers=header, - json=body, timeout=10) + r = requests.post(self.__url, headers=header, json=body, timeout=10) r.raise_for_status() logger.info("Receipt template sent successfully - %d", 200) return jsonify(r.json(), 200) diff --git a/pynani/__init__.py b/pynani/__init__.py index 5c267d1..7da1707 100644 --- a/pynani/__init__.py +++ b/pynani/__init__.py @@ -1,4 +1,4 @@ from .Messenger import Messenger -from .utils.QuickReply import QuickReply -from .utils.Buttons import Buttons -from .utils import elements +from .utils.quick_reply import quick_buttons, quick_image_buttons +from .utils.buttons import basic_buttons, exit_buttons +from .utils.receipt import get_address, get_elements, get_adjustments, get_summary diff --git a/pynani/utils/Buttons.py b/pynani/utils/Buttons.py index 60f31a5..de0808d 100644 --- a/pynani/utils/Buttons.py +++ b/pynani/utils/Buttons.py @@ -77,7 +77,7 @@ def basic_buttons( buttons: Union[str, List[Union[str, int]]]) -> List: buttons = buttons[:3] return [__make_button(button) for button in buttons] -def leave_buttons( buttons: Union[Dict, List[Dict]]) -> List: +def exit_buttons( buttons: Union[Dict, List[Dict]]) -> List: """ Creates a list of leave buttons. diff --git a/pynani/utils/quick_reply.py b/pynani/utils/quick_reply.py index 59eb105..638c9a6 100644 --- a/pynani/utils/quick_reply.py +++ b/pynani/utils/quick_reply.py @@ -39,12 +39,12 @@ def quick_buttons(buttons: List[Union[str, int]]) -> List[Dict]: Example: >>> quick_buttons(["Hello", "World"]) - [{'content_type': 'text', 'title': 'Hello', 'payload': '', 'image_url': ''}, - {'content_type': 'text', 'title': 'World', 'payload': '', 'image_url': ''}] + [{'content_type': 'text', 'title': 'Hello', 'payload': '', 'image_url': None}, + {'content_type': 'text', 'title': 'World', 'payload': '', 'image_url': None}] >>> quick_buttons([1, 2, 3]) - [{'content_type': 'text', 'title': '1', 'payload': '', 'image_url': ''}, - {'content_type': 'text', 'title': '2', 'payload': '', 'image_url': ''}, - {'content_type': 'text', 'title': '3', 'payload': '', 'image_url': ''}] + [{'content_type': 'text', 'title': '1', 'payload': '', 'image_url': None}, + {'content_type': 'text', 'title': '2', 'payload': '', 'image_url': None}, + {'content_type': 'text', 'title': '3', 'payload': '', 'image_url': None}] """ r_buttons = [] @@ -57,34 +57,31 @@ def quick_buttons(buttons: List[Union[str, int]]) -> List[Dict]: return r_buttons -def quick_buttons_image(buttons: List) -> List[Dict]: +def quick_image_buttons(buttons: List[Union[str, int]], images: List[str]) -> List[Dict]: """ - Prepares a list of quick reply buttons with images from a list of dictionaries. + Prepares a list of quick reply buttons from a list of strings. Args: - buttons (List): A list of dictionaries representing the quick reply buttons with their properties. + buttons (List[str]): A list of strings or integers representing the text for each quick reply button. + images (List[str]): A list of strings representing the image URL for each quick reply button. Returns: - List: A list of dictionaries representing the quick reply buttons with their properties, including images. - - Raises: - ValueError: If each button is not a dictionary. + List: A list of dictionaries representing the quick reply buttons with their properties. Example: - >>> quick_buttons_image([{"text": "Hello", "image_url": "https://photos.com/hello.jpg"}, {"text": "World", "image_url": "https://photos.com/world.jpg"}]) + >>> quick_image_buttons(["Hello", "World"], ["https://photos.com/hello.jpg", "https://photos.com/world.jpg"]) [{'content_type': 'text', 'title': 'Hello', 'payload': '', 'image_url': 'https://photos.com/hello.jpg'}, {'content_type': 'text', 'title': 'World', 'payload': '', 'image_url': 'https://photos.com/world.jpg'}] + If a button does not have an image, the image URL should be None or an empty string. + >>> quick_image_buttons(["Hello", "World", "Yes"], ["https://photos.com/hello.jpg", "https://photos.com/world.jpg", None]) """ r_buttons = [] if len(buttons) > 13: logger.warning("Quick replies should be less than 13") buttons = buttons[:13] - for b in buttons: - if not isinstance(b, dict): - logger.error("Each button should be a dictionary") - raise ValueError("Each button should be a dictionary") - else: - r_buttons.append(__make_quick_button(**b)) + + for b, i in zip(buttons, images): + r_buttons.append(__make_quick_button(b, i)) return r_buttons From 0edd653c10734a7e849b4f63dad18fc9cf370606 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Mon, 24 Jun 2024 22:51:38 +0000 Subject: [PATCH 14/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/utils/buttons.py | 104 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 pynani/utils/buttons.py diff --git a/pynani/utils/buttons.py b/pynani/utils/buttons.py new file mode 100644 index 0000000..de0808d --- /dev/null +++ b/pynani/utils/buttons.py @@ -0,0 +1,104 @@ +from typing import Optional, Union, List, Dict +from .logs import logger + + +def __make_button( title: str, url: Optional[str] = None, call_number: Optional[str] = None) -> Dict: + """ + Creates a button with the specified title and optional URL or call number. + + Args: + title (str): The title of the button. + url (Optional[str], optional): The URL associated with the button. Defaults to None. + call_number (Optional[str], optional): The call number associated with the button. Defaults to None. + + Returns: + Dict: A dictionary representing the button with its properties. + + Raises: + ValueError: If both URL and call number are provided. + + Example: + >>> __make_button("Hola", "https://www.google.com") + {'type': 'web_url', 'title': 'Hola', 'payload': '', 'url': 'https://www.google.com'} + >>> __make_button("Mundo", call_number="+525555555555") + {'type': 'phone_number', 'title': 'Mundo', 'payload': '+525555555555', 'url': ''} + >>> __make_button("Hello") + {'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''} + """ + + if url is not None and call_number is not None: + logger.error("You can't have both url and call_number at the same time") + raise ValueError("You can't have both url and call_number at the same time") + + if url: + type_button = "web_url" + url_button = url + payload_button = "" + elif call_number: + type_button = "phone_number" + url_button = "" + payload_button = call_number + elif not url and not call_number: + type_button = "postback" + url_button = "" + payload_button = "DEVELOPER_DEFINED_PAYLOAD" + + return { + "type": type_button, + "title": title, + "payload": payload_button, + "url": url_button, + } + +def basic_buttons( buttons: Union[str, List[Union[str, int]]]) -> List: + """ + Creates a list of basic buttons. + + Args: + buttons (Union[str, List[Union[str, int]]]): The buttons to be created. It can be a string or a list of strings or integers. + + Returns: + List: A list of dictionaries representing the basic buttons with their properties. + + Example: + >>> basic_buttons(["Hello", "World", "馃敟"]) + [{'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}, + {'type': 'postback', 'title': 'World', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}, + {'type': 'postback', 'title': '馃敟', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}] + >>> basic_buttons("Hello") + [{'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}] + """ + + if isinstance(buttons, str): + return [__make_button(buttons)] + else: + if len(buttons) > 3: + logger.warning("Buttons template should be less than 3") + buttons = buttons[:3] + return [__make_button(button) for button in buttons] + +def exit_buttons( buttons: Union[Dict, List[Dict]]) -> List: + """ + Creates a list of leave buttons. + + Args: + buttons (Union[Dict, List[Dict]]): The buttons to be created. It can be a dictionary or a list of dictionaries. + + Returns: + List: A list of dictionaries representing the leave buttons with their properties. + + Example: + >>> leave_buttons([{"title": "Hello", "url": "https://www.google.com"}, {"title": "World", "call_number": "+525555555555"}]) + [{'type': 'web_url', 'title': 'Hello', 'payload': '', 'url': 'https://www.google.com'}, + {'type': 'phone_number', 'title': 'World', 'payload': '+525555555555', 'url': ''}] + >>> leave_buttons({"title": "Hello", "url": "https://www.google.com"}) + [{'type': 'web_url', 'title': 'Hello', 'payload': '', 'url': 'https://www.google.com'}] + """ + + if isinstance(buttons, dict): + return [__make_button(**buttons)] + else: + if len(buttons) > 3: + logger.warning("Buttons template should be less than 3") + buttons = buttons[:3] + return [__make_button(**button) for button in buttons] From ce19821a45c8e4ab268bfd879813e0ac98ec364b Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Tue, 25 Jun 2024 02:04:44 +0000 Subject: [PATCH 15/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5ad7f8a..2628a4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,14 +27,15 @@ classifiers = [ ] dependencies = [ "requests>=2.32.3", + "colorlog>=6.8.2", ] description = "A package to wrap the Messenger API" -keywords = ["Messenger", "wrapper", "Meta", "API", "Facebook", "Instagram"] +keywords = ["Messenger", "wrapper", "Meta", "API", "Facebook", "Pynani", "PyMessenger"] license = {file = "LICENSE"} name = "pynani" readme = "README.md" requires-python = ">=3.8" -version = "1.2.1" +version = "1.3.0" [project.urls] "Bug Tracker" = "https://github.com/jorge-jrzz/Pynani/issues" From d1f4be3f7990a48c45a5dffeadb3e08b7b916b39 Mon Sep 17 00:00:00 2001 From: Jorge Juarez Date: Mon, 24 Jun 2024 20:09:59 -0600 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=94=A5=20delete=20unused=20=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/utils/QuickReply.py | 42 ------------------------------------ pynani/utils/elements.py | 44 -------------------------------------- 2 files changed, 86 deletions(-) delete mode 100644 pynani/utils/QuickReply.py delete mode 100644 pynani/utils/elements.py diff --git a/pynani/utils/QuickReply.py b/pynani/utils/QuickReply.py deleted file mode 100644 index d07c5e5..0000000 --- a/pynani/utils/QuickReply.py +++ /dev/null @@ -1,42 +0,0 @@ -class QuickReply(): - def __make_quick_button(self, text: str, payload: str = "", image_url: str = None) -> dict: - return { - "content_type": "text", - "title": text, - "payload": payload, - "image_url": image_url if image_url else "", - } - - def quick_buttons(self, buttons: list) -> list: - r_buttons = [] - if len(buttons) > 13: - print("Quick replies should be less than 13") - buttons = buttons[:13] - - for b in buttons: - if not isinstance(b, str): - raise ValueError("Each button should be a string") - else: - r_buttons.append(self.__make_quick_button(b)) - - return r_buttons - - def quick_buttons_image(self, buttons: list) -> list: - r_buttons = [] - if len(buttons) > 13: - print("Quick replies should be less than 13") - buttons = buttons[:13] - for b in buttons: - if not isinstance(b, dict): - raise ValueError("Each button should be a dictionary") - else: - r_buttons.append(self.__make_quick_button(**b)) - - return r_buttons - - -# botones = ['pedro', 'juan', 'popo'] -# b_imagen = [{"text": "1", "image_url": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Solid_red.png"}, -# {"text": "2", "image_url": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Solid_red.png"}] -# sb = qui.quick_buttons(botones) -# cb = qui.quick_buttons_image(b_imagen) diff --git a/pynani/utils/elements.py b/pynani/utils/elements.py deleted file mode 100644 index 6442746..0000000 --- a/pynani/utils/elements.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Optional, List, Any - - -def get_address(street_1: str, city: str, postal_code: str, state: str, country: str, street_2: Optional[str] = None) -> dict: - return { - "street_1": street_1, - "street_2": street_2, - "city": city, - "postal_code": postal_code, - "state": state, - "country": country - } - - -def get_summary(total_cost: float, subtotal: Optional[float] = None, shipping_cost: Optional[float] = None, total_tax: Optional[float] = None): - return { - "subtotal": subtotal, - "shipping_cost": shipping_cost, - "total_tax": total_tax, - "total_cost": total_cost - } - - -def get_adjustments(*args: Any) -> list: - adjustments = [] - for i in range(0, len(args), 2): - adjustment = { - "name": args[i], - "amount": args[i + 1] - } - adjustments.append(adjustment) - return adjustments - - -def get_elements(title: str, price: float, subtitle: Optional[str] = None, quantity: Optional[int] = None, currency: Optional[str] = 'USD', image_url: Optional[str] = None) -> List[dict]: - item = { - "title": title, - "subtitle": subtitle, - "quantity": quantity, - "price": price, - "currency": currency, - "image_url": image_url - } - return [item] From 7a153dd5d5f10ca96f676a452368ab50bda444d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20=C3=81ngel=20Ju=C3=A1rez=20V=C3=A1zquez?= Date: Mon, 24 Jun 2024 20:11:11 -0600 Subject: [PATCH 17/22] Delete pynani/utils/Buttons.py --- pynani/utils/Buttons.py | 104 ---------------------------------------- 1 file changed, 104 deletions(-) delete mode 100644 pynani/utils/Buttons.py diff --git a/pynani/utils/Buttons.py b/pynani/utils/Buttons.py deleted file mode 100644 index de0808d..0000000 --- a/pynani/utils/Buttons.py +++ /dev/null @@ -1,104 +0,0 @@ -from typing import Optional, Union, List, Dict -from .logs import logger - - -def __make_button( title: str, url: Optional[str] = None, call_number: Optional[str] = None) -> Dict: - """ - Creates a button with the specified title and optional URL or call number. - - Args: - title (str): The title of the button. - url (Optional[str], optional): The URL associated with the button. Defaults to None. - call_number (Optional[str], optional): The call number associated with the button. Defaults to None. - - Returns: - Dict: A dictionary representing the button with its properties. - - Raises: - ValueError: If both URL and call number are provided. - - Example: - >>> __make_button("Hola", "https://www.google.com") - {'type': 'web_url', 'title': 'Hola', 'payload': '', 'url': 'https://www.google.com'} - >>> __make_button("Mundo", call_number="+525555555555") - {'type': 'phone_number', 'title': 'Mundo', 'payload': '+525555555555', 'url': ''} - >>> __make_button("Hello") - {'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''} - """ - - if url is not None and call_number is not None: - logger.error("You can't have both url and call_number at the same time") - raise ValueError("You can't have both url and call_number at the same time") - - if url: - type_button = "web_url" - url_button = url - payload_button = "" - elif call_number: - type_button = "phone_number" - url_button = "" - payload_button = call_number - elif not url and not call_number: - type_button = "postback" - url_button = "" - payload_button = "DEVELOPER_DEFINED_PAYLOAD" - - return { - "type": type_button, - "title": title, - "payload": payload_button, - "url": url_button, - } - -def basic_buttons( buttons: Union[str, List[Union[str, int]]]) -> List: - """ - Creates a list of basic buttons. - - Args: - buttons (Union[str, List[Union[str, int]]]): The buttons to be created. It can be a string or a list of strings or integers. - - Returns: - List: A list of dictionaries representing the basic buttons with their properties. - - Example: - >>> basic_buttons(["Hello", "World", "馃敟"]) - [{'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}, - {'type': 'postback', 'title': 'World', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}, - {'type': 'postback', 'title': '馃敟', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}] - >>> basic_buttons("Hello") - [{'type': 'postback', 'title': 'Hello', 'payload': 'DEVELOPER_DEFINED_PAYLOAD', 'url': ''}] - """ - - if isinstance(buttons, str): - return [__make_button(buttons)] - else: - if len(buttons) > 3: - logger.warning("Buttons template should be less than 3") - buttons = buttons[:3] - return [__make_button(button) for button in buttons] - -def exit_buttons( buttons: Union[Dict, List[Dict]]) -> List: - """ - Creates a list of leave buttons. - - Args: - buttons (Union[Dict, List[Dict]]): The buttons to be created. It can be a dictionary or a list of dictionaries. - - Returns: - List: A list of dictionaries representing the leave buttons with their properties. - - Example: - >>> leave_buttons([{"title": "Hello", "url": "https://www.google.com"}, {"title": "World", "call_number": "+525555555555"}]) - [{'type': 'web_url', 'title': 'Hello', 'payload': '', 'url': 'https://www.google.com'}, - {'type': 'phone_number', 'title': 'World', 'payload': '+525555555555', 'url': ''}] - >>> leave_buttons({"title": "Hello", "url": "https://www.google.com"}) - [{'type': 'web_url', 'title': 'Hello', 'payload': '', 'url': 'https://www.google.com'}] - """ - - if isinstance(buttons, dict): - return [__make_button(**buttons)] - else: - if len(buttons) > 3: - logger.warning("Buttons template should be less than 3") - buttons = buttons[:3] - return [__make_button(**button) for button in buttons] From cc47b5233e166c231306fc62eb4c2e8e0a60d179 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Tue, 25 Jun 2024 15:25:47 +0000 Subject: [PATCH 18/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/Messenger.py | 38 +++++++++++++++++++++++++++++++++++++- pynani/verify_token.html | 34 ---------------------------------- 2 files changed, 37 insertions(+), 35 deletions(-) delete mode 100644 pynani/verify_token.html diff --git a/pynani/Messenger.py b/pynani/Messenger.py index 5e069c5..d31e537 100644 --- a/pynani/Messenger.py +++ b/pynani/Messenger.py @@ -33,6 +33,42 @@ def jsonify(data: Union[Dict, str], status_code: int) -> Tuple: return data.encode('utf-8'), status_code, {'Content-Type': 'text/html'} +HOOK_PAGE = """ + + + + + Verificaci贸n de Token + + + +
+

Hello, World!

+

This is the endpoint to verify the token 馃攼馃敆

+
+ +""" + + class Messenger(): """ Initializes the Messenger class with the provided access token and page ID. @@ -78,7 +114,7 @@ def verify_token(self, params: Dict, token: str) -> Tuple: logger.info('Verification successful - %d', 200) return jsonify(challenge, 200) logger.warning('This endpoint is to verify token - %d', 200,) - return jsonify(Path("pynani/verify_token.html").read_text(encoding='utf-8'), 200) + return jsonify(HOOK_PAGE, 200) def get_sender_id(self, data: dict) -> Optional[str]: diff --git a/pynani/verify_token.html b/pynani/verify_token.html deleted file mode 100644 index 42ad488..0000000 --- a/pynani/verify_token.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - Verificaci贸n de Token - - - -
-

Hello, World!

-

This is the endpoint to verify the token 馃攼馃敆

-
- - From d3d03ae98e4ed0baec52a139a3dd811b8a8a9a89 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Tue, 25 Jun 2024 21:37:52 +0000 Subject: [PATCH 19/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/Messenger.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pynani/Messenger.py b/pynani/Messenger.py index d31e537..ab47b4a 100644 --- a/pynani/Messenger.py +++ b/pynani/Messenger.py @@ -256,8 +256,7 @@ def upload_attachment(self, attachment_type: str, attachment_path: str) -> str: "1234567897654321" """ - attachments_url = f"https://graph.facebook.com/v20.0/{ - self.page_id}/message_attachments" + attachments_url = f"https://graph.facebook.com/v20.0/{self.page_id}/message_attachments" attachment = Path(attachment_path) mimetype, _ = mimetypes.guess_type(attachment) From 78bd7110680d4d18dc988dd49ce4917176c23d64 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Thu, 27 Jun 2024 00:17:02 +0000 Subject: [PATCH 20/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- pyproject.toml | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6738403..eb0a65c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -banner Pynani +banner Pynani --- diff --git a/pyproject.toml b/pyproject.toml index 2628a4a..07aa0e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,8 +17,6 @@ classifiers = [ "Topic :: Utilities", "License :: OSI Approved :: MIT License", # "Topic :: Documentation :: Sphinx" "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -34,8 +32,8 @@ keywords = ["Messenger", "wrapper", "Meta", "API", "Facebook", "Pynani", "PyMess license = {file = "LICENSE"} name = "pynani" readme = "README.md" -requires-python = ">=3.8" -version = "1.3.0" +requires-python = ">=3.10" +version = "1.3.1" [project.urls] "Bug Tracker" = "https://github.com/jorge-jrzz/Pynani/issues" From 935cbf06b203e6199383aced68d2cc86bf05d3ed Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Fri, 12 Jul 2024 06:24:23 +0000 Subject: [PATCH 21/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynani/Messenger.py | 73 ++++++++++++++++++++++++++++--------- pynani/__init__.py | 2 +- pynani/utils/__init__.py | 2 +- pynani/utils/buttons.py | 4 +- pynani/utils/logs.py | 37 ++++++++++--------- pynani/utils/quick_reply.py | 4 +- 6 files changed, 82 insertions(+), 40 deletions(-) diff --git a/pynani/Messenger.py b/pynani/Messenger.py index ab47b4a..3516b1c 100644 --- a/pynani/Messenger.py +++ b/pynani/Messenger.py @@ -12,26 +12,10 @@ from typing import Union, Optional, Tuple, Dict, List import requests from requests.exceptions import RequestException -from .utils import logger +from .utils import get_logger -def jsonify(data: Union[Dict, str], status_code: int) -> Tuple: - """ - Converts the given data to a JSON response with the specified status code. - - Args: - data (Union[Dict, str]): The data to be converted to JSON. It can be a dictionary or a string. - status_code (int): The HTTP status code to be returned with the response. - - Returns: - Tuple: A tuple containing the JSON response, the status code, and the headers. - """ - - if isinstance(data, dict): - return json.dumps(data), status_code, {'Content-Type': 'application/json'} - elif isinstance(data, str): - return data.encode('utf-8'), status_code, {'Content-Type': 'text/html'} - +logger = get_logger(__name__) HOOK_PAGE = """ @@ -69,6 +53,59 @@ def jsonify(data: Union[Dict, str], status_code: int) -> Tuple: """ +def jsonify(data: Union[Dict, str], status_code: int) -> Tuple: + """ + Converts the given data to a JSON response with the specified status code. + + Args: + data (Union[Dict, str]): The data to be converted to JSON. It can be a dictionary or a string. + status_code (int): The HTTP status code to be returned with the response. + + Returns: + Tuple: A tuple containing the JSON response, the status code, and the headers. + """ + + if isinstance(data, dict): + return json.dumps(data), status_code, {'Content-Type': 'application/json'} + elif isinstance(data, str): + return data.encode('utf-8'), status_code, {'Content-Type': 'text/html'} + + +def get_long_lived_token(app_id: str, app_secret: str, short_lived_token: str, save_env: Optional[bool] = False) -> Optional[str]: + """ + Obtains a long-lived access token using the provided short-lived token. + + Args: + app_id (str): The application ID. + app_secret (str): The application secret. + short_lived_token (str): The short-lived page access token. + save_env (Optional[bool], optional): Whether to save the long-lived token to the .env file. Defaults to False. + + Returns: + Optional[str]: The long-lived access token if successful, otherwise None. + + Exception: + RequestException: If an error occurs during the request. + + Example: + >>> get_long_lived_token('765xxx', 'e0fcxxx', 'EAAK4AXXXX', True) + "EAAK4XXXX" + """ + + url = f"https://graph.facebook.com/v20.0/oauth/access_token?grant_type=fb_exchange_token&client_id={app_id}&client_secret={app_secret}&fb_exchange_token={short_lived_token}" + try: + r = requests.get(url, timeout=10) + r.raise_for_status() + logger.info("Long-lived token obtained successfully - %d", 200) + if save_env: + with open('.env', 'a', encoding='utf-8') as file: + file.write(f'\nACCESS_TOKEN={r.json()["access_token"]}') + return r.json()['access_token'] + except RequestException as e: + logger.error("%s - %d", e, 403) + return None + + class Messenger(): """ Initializes the Messenger class with the provided access token and page ID. diff --git a/pynani/__init__.py b/pynani/__init__.py index 7da1707..f6bd973 100644 --- a/pynani/__init__.py +++ b/pynani/__init__.py @@ -1,4 +1,4 @@ -from .Messenger import Messenger +from .Messenger import Messenger, get_long_lived_token from .utils.quick_reply import quick_buttons, quick_image_buttons from .utils.buttons import basic_buttons, exit_buttons from .utils.receipt import get_address, get_elements, get_adjustments, get_summary diff --git a/pynani/utils/__init__.py b/pynani/utils/__init__.py index c1c2a87..b252b9f 100644 --- a/pynani/utils/__init__.py +++ b/pynani/utils/__init__.py @@ -1 +1 @@ -from .logs import logger \ No newline at end of file +from .logs import get_logger \ No newline at end of file diff --git a/pynani/utils/buttons.py b/pynani/utils/buttons.py index de0808d..76a50aa 100644 --- a/pynani/utils/buttons.py +++ b/pynani/utils/buttons.py @@ -1,7 +1,9 @@ from typing import Optional, Union, List, Dict -from .logs import logger +from .logs import get_logger +logger = get_logger(__name__) + def __make_button( title: str, url: Optional[str] = None, call_number: Optional[str] = None) -> Dict: """ Creates a button with the specified title and optional URL or call number. diff --git a/pynani/utils/logs.py b/pynani/utils/logs.py index 3749bcb..c7237a9 100644 --- a/pynani/utils/logs.py +++ b/pynani/utils/logs.py @@ -1,25 +1,26 @@ """This file is used to configure the logger for the project""" - import logging from colorlog import ColoredFormatter -logger = logging.getLogger('Pynani') -logger.setLevel(logging.DEBUG) -formatter = ColoredFormatter( - "%(log_color)s%(levelname)s: %(name)s [%(asctime)s] -- %(message)s", - datefmt='%d/%m/%Y %H:%M:%S', - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'bold_red', - } -) -console_handler = logging.StreamHandler() -console_handler.setLevel(logging.DEBUG) -console_handler.setFormatter(formatter) +def get_logger(name: str) -> logging.Logger: + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + formatter = ColoredFormatter( + "%(log_color)s%(levelname)s: %(name)s [%(asctime)s] -- %(message)s", + datefmt='%d/%m/%Y %H:%M:%S', + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'bold_red', + } + ) + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_handler.setFormatter(formatter) -logger.addHandler(console_handler) + logger.addHandler(console_handler) + return logger diff --git a/pynani/utils/quick_reply.py b/pynani/utils/quick_reply.py index 638c9a6..a8d74eb 100644 --- a/pynani/utils/quick_reply.py +++ b/pynani/utils/quick_reply.py @@ -1,7 +1,9 @@ from typing import Optional, Union, List, Dict -from .logs import logger +from .logs import get_logger +logger = get_logger(__name__) + def __make_quick_button(text: Union[str, int], image_url: Optional[str] = None) -> Dict: """ Creates a quick reply button with the specified text, optional image URL, and payload. From d09e5bf9d35a996645be4bff8d1f99bfac9cabb7 Mon Sep 17 00:00:00 2001 From: jorge-jrzz Date: Fri, 12 Jul 2024 06:41:33 +0000 Subject: [PATCH 22/22] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 07aa0e4..b16b949 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ authors = [ {name = "Jorge Juarez", email = "jorgeang33@gmail.com"}, ] classifiers = [ - "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Customer Service", "Topic :: Software Development :: Build Tools", @@ -15,7 +14,8 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Version Control :: Git", "Topic :: Utilities", - "License :: OSI Approved :: MIT License", # "Topic :: Documentation :: Sphinx" + "Topic :: Documentation :: Sphinx", + "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -33,7 +33,7 @@ license = {file = "LICENSE"} name = "pynani" readme = "README.md" requires-python = ">=3.10" -version = "1.3.1" +version = "1.4.0" [project.urls] "Bug Tracker" = "https://github.com/jorge-jrzz/Pynani/issues"