├── .github └── workflows │ └── ci.yml ├── .gitignore ├── MANIFEST.in ├── README.md ├── images ├── add_recipient_phone_number.png ├── api_getting_started.png ├── click_on_quick_start.png ├── conf_webhook_2.png ├── conf_webhook_3.png ├── configure_webhooks.png ├── recipient_to_development_mode.png └── waba_settings.png ├── license.txt ├── pyproject.toml └── waba_integration ├── __init__.py ├── api └── webhook.py ├── config ├── __init__.py ├── desktop.py └── docs.py ├── hooks.py ├── modules.txt ├── patches.txt ├── public └── .gitkeep ├── templates ├── __init__.py └── pages │ └── __init__.py ├── whatsapp_business_api_integration ├── __init__.py ├── doctype │ ├── __init__.py │ ├── waba_message_status_update │ │ ├── __init__.py │ │ ├── test_waba_message_status_update.py │ │ ├── waba_message_status_update.js │ │ ├── waba_message_status_update.json │ │ └── waba_message_status_update.py │ ├── waba_settings │ │ ├── __init__.py │ │ ├── test_waba_settings.py │ │ ├── waba_settings.js │ │ ├── waba_settings.json │ │ └── waba_settings.py │ ├── waba_webhook_log │ │ ├── __init__.py │ │ ├── test_waba_webhook_log.py │ │ ├── waba_webhook_log.js │ │ ├── waba_webhook_log.json │ │ └── waba_webhook_log.py │ ├── waba_whatsapp_contact │ │ ├── __init__.py │ │ ├── test_waba_whatsapp_contact.py │ │ ├── waba_whatsapp_contact.js │ │ ├── waba_whatsapp_contact.json │ │ └── waba_whatsapp_contact.py │ └── waba_whatsapp_message │ │ ├── __init__.py │ │ ├── test_waba_whatsapp_message.py │ │ ├── waba_whatsapp_message.js │ │ ├── waba_whatsapp_message.json │ │ └── waba_whatsapp_message.py └── workspace │ └── whatsapp │ └── whatsapp.json └── www └── __init__.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - develop 8 | pull_request: 9 | 10 | concurrency: 11 | group: develop-waba_integration-${{ github.event.number }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | tests: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | name: Server 20 | 21 | services: 22 | mariadb: 23 | image: mariadb:10.6 24 | env: 25 | MYSQL_ROOT_PASSWORD: root 26 | ports: 27 | - 3306:3306 28 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 29 | 30 | steps: 31 | - name: Clone 32 | uses: actions/checkout@v2 33 | 34 | - name: Setup Python 35 | uses: actions/setup-python@v2 36 | with: 37 | python-version: 3.11 38 | 39 | - name: Setup Node 40 | uses: actions/setup-node@v2 41 | with: 42 | node-version: 18 43 | check-latest: true 44 | 45 | - name: Cache pip 46 | uses: actions/cache@v2 47 | with: 48 | path: ~/.cache/pip 49 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt') }} 50 | restore-keys: | 51 | ${{ runner.os }}-pip- 52 | ${{ runner.os }}- 53 | 54 | - name: Get yarn cache directory path 55 | id: yarn-cache-dir-path 56 | run: 'echo "::set-output name=dir::$(yarn cache dir)"' 57 | 58 | - uses: actions/cache@v2 59 | id: yarn-cache 60 | with: 61 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 62 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 63 | restore-keys: | 64 | ${{ runner.os }}-yarn- 65 | 66 | - name: Setup 67 | run: | 68 | pip install frappe-bench 69 | bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench 70 | mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" 71 | mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" 72 | 73 | - name: Install 74 | working-directory: /home/runner/frappe-bench 75 | run: | 76 | bench get-app waba_integration $GITHUB_WORKSPACE 77 | bench setup requirements --dev 78 | bench new-site --db-root-password root --admin-password admin test_site 79 | bench --site test_site install-app waba_integration 80 | bench build 81 | env: 82 | CI: 'Yes' 83 | 84 | - name: Run Tests 85 | working-directory: /home/runner/frappe-bench 86 | run: | 87 | bench --site test_site set-config allow_tests true 88 | bench --site test_site run-tests --app waba_integration 89 | env: 90 | TYPE: server 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | waba_integration/docs/current 7 | node_modules/ -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include waba_integration *.css 8 | recursive-include waba_integration *.csv 9 | recursive-include waba_integration *.html 10 | recursive-include waba_integration *.ico 11 | recursive-include waba_integration *.js 12 | recursive-include waba_integration *.json 13 | recursive-include waba_integration *.md 14 | recursive-include waba_integration *.png 15 | recursive-include waba_integration *.py 16 | recursive-include waba_integration *.svg 17 | recursive-include waba_integration *.txt 18 | recursive-exclude waba_integration *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## WhatsApp Business API Integration 2 | 3 | Work with WhatsApp Business Cloud API from your Frappe site. Automate sending and releasing of WhatsApp messages. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | bench get-app waba_integration 9 | bench --site install-app waba_integration 10 | ``` 11 | 12 | If you want to install on Frappe Cloud, presently, you can fork and add it to your private bench. 13 | 14 | ## Getting Started 15 | 16 | In order to set up the app, you will need to first setup developer assets and get credentials from the **Meta Developer Portal**. You can do so by following [this guide](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#set-up-developer-assets). 17 | 18 | In developer mode, if you want to send a message to a number, you have to explicitly add it (also verified by an OTP) to the recipient's list: 19 | 20 | ![](images/recipient_to_development_mode.png) 21 | 22 | ![](images/add_recipient_phone_number.png) 23 | 24 | Now, try sending a test message from the developer dashboard by clicking the `Send Message` button: 25 | 26 | ![](images/api_getting_started.png) 27 | 28 | If you received the message, you are set! 29 | 30 | ### Configuring Webhook 31 | 32 | Step 1: 33 | 34 | ![Alt text](images/configure_webhooks.png) 35 | 36 | Step 2: 37 | 38 | ![Alt text](images/conf_webhook_2.png) 39 | 40 | Step 3: 41 | 42 | ![Alt text](images/conf_webhook_3.png) 43 | 44 | The callback URL will be: 45 | 46 | ``` 47 | /api/method/waba_integration.api.webhook.handle 48 | ``` 49 | 50 | For instance: 51 | 52 | ``` 53 | https://hussain.frappe.cloud/api/method/waba_integration.api.webhook.handle 54 | ``` 55 | 56 | You can set the verify token to any random string, but remember it, since we will need it in the next step. 57 | 58 | > Note: For working with webhooks in the development environment, you will need to use ngrok to proxy the webhooks to your local. 59 | 60 | ## Setting up the app 61 | 62 | Open `WABA Settings` form and fill in the credentials you obtained in the last step from the Meta developer console: 63 | 64 | ![Alt text](images/waba_settings.png) 65 | 66 | The **Webhook Verify Token** must be same as the verify token you set in the developer console's webhook configuration in the previous step. 67 | 68 | ### Permanent Token / Production Setup 69 | 70 | The temporary token generated above is only valid for 24 hours and is only suitable for development. In order to generate a permanent token, please refer [this](https://developers.facebook.com/docs/whatsapp/business-management-api/get-started#1--acquire-an-access-token-using-a-system-user-or-facebook-login) guide. 71 | 72 | ## Sending Your First Message 73 | 74 | You can use the **WABA WhatsApp Message** doctype to create and send messages. Whenever you receive a new message, you will find it here. 75 | 76 | ## Debugging / Webhook Logs 77 | 78 | Use the **WABA Webhook Log** to see all the webhooks received from WhatsApp Cloud API. You can use this for debugging and also you can write hooks on top of it to build your own integrations. 79 | 80 | #### License 81 | 82 | MIT 83 | -------------------------------------------------------------------------------- /images/add_recipient_phone_number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/images/add_recipient_phone_number.png -------------------------------------------------------------------------------- /images/api_getting_started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/images/api_getting_started.png -------------------------------------------------------------------------------- /images/click_on_quick_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/images/click_on_quick_start.png -------------------------------------------------------------------------------- /images/conf_webhook_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/images/conf_webhook_2.png -------------------------------------------------------------------------------- /images/conf_webhook_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/images/conf_webhook_3.png -------------------------------------------------------------------------------- /images/configure_webhooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/images/configure_webhooks.png -------------------------------------------------------------------------------- /images/recipient_to_development_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/images/recipient_to_development_mode.png -------------------------------------------------------------------------------- /images/waba_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/images/waba_settings.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | License: MIT -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "waba_integration" 3 | authors = [ 4 | { name = "Hussain Nagaria", email = "hussain@frappe.io"} 5 | ] 6 | description = "Work with WhatsApp Business Cloud API from your Frappe site" 7 | requires-python = ">=3.10" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | dependencies = [] 11 | 12 | [build-system] 13 | requires = ["flit_core >=3.4,<4"] 14 | build-backend = "flit_core.buildapi" 15 | 16 | [tool.black] 17 | line-length = 99 18 | 19 | [tool.isort] 20 | line_length = 99 21 | multi_line_output = 3 22 | include_trailing_comma = true 23 | force_grid_wrap = 0 24 | use_parentheses = true 25 | ensure_newline_before_comments = true 26 | indent = "\t" 27 | -------------------------------------------------------------------------------- /waba_integration/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.0.1' 3 | 4 | -------------------------------------------------------------------------------- /waba_integration/api/webhook.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from waba_integration.whatsapp_business_api_integration.doctype.waba_whatsapp_message.waba_whatsapp_message import ( 3 | create_waba_whatsapp_message, 4 | process_status_update, 5 | ) 6 | 7 | from werkzeug.wrappers import Response 8 | 9 | 10 | @frappe.whitelist(allow_guest=True) 11 | def handle(): 12 | if frappe.request.method == "GET": 13 | return verify_token_and_fulfill_challenge() 14 | 15 | try: 16 | form_dict = frappe.local.form_dict 17 | messages = form_dict["entry"][0]["changes"][0]["value"].get("messages", []) 18 | statuses = form_dict["entry"][0]["changes"][0]["value"].get("statuses", []) 19 | 20 | for status in statuses: 21 | process_status_update(status) 22 | 23 | for message in messages: 24 | create_waba_whatsapp_message(message) 25 | 26 | frappe.get_doc( 27 | {"doctype": "WABA Webhook Log", "payload": frappe.as_json(form_dict)} 28 | ).insert(ignore_permissions=True) 29 | except Exception: 30 | frappe.log_error("WABA Webhook Log Error", frappe.get_traceback()) 31 | frappe.throw("Something went wrong") 32 | 33 | 34 | def verify_token_and_fulfill_challenge(): 35 | meta_challenge = frappe.form_dict.get("hub.challenge") 36 | expected_token = frappe.db.get_single_value("WABA Settings", "webhook_verify_token") 37 | 38 | if frappe.form_dict.get("hub.verify_token") != expected_token: 39 | frappe.throw("Verify token does not match") 40 | 41 | return Response(meta_challenge, status=200) 42 | -------------------------------------------------------------------------------- /waba_integration/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/config/__init__.py -------------------------------------------------------------------------------- /waba_integration/config/desktop.py: -------------------------------------------------------------------------------- 1 | from frappe import _ 2 | 3 | def get_data(): 4 | return [ 5 | { 6 | "module_name": "WhatsApp Bussiness API Integration", 7 | "type": "module", 8 | "label": _("WhatsApp Bussiness API Integration") 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /waba_integration/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/waba_integration" 6 | # headline = "App that does everything" 7 | # sub_heading = "Yes, you got that right the first time, everything" 8 | 9 | def get_context(context): 10 | context.brand_html = "WhatsApp Bussiness API Integration" 11 | -------------------------------------------------------------------------------- /waba_integration/hooks.py: -------------------------------------------------------------------------------- 1 | from . import __version__ as app_version 2 | 3 | app_name = "waba_integration" 4 | app_title = "WhatsApp Business API Integration" 5 | app_publisher = "Hussain Nagaria" 6 | app_description = "Work with WhatsApp Business Cloud API from your Frappe site" 7 | app_email = "hussain@frappe.io" 8 | app_license = "MIT" 9 | 10 | # Includes in 11 | # ------------------ 12 | 13 | # include js, css files in header of desk.html 14 | # app_include_css = "/assets/waba_integration/css/waba_integration.css" 15 | # app_include_js = "/assets/waba_integration/js/waba_integration.js" 16 | 17 | # include js, css files in header of web template 18 | # web_include_css = "/assets/waba_integration/css/waba_integration.css" 19 | # web_include_js = "/assets/waba_integration/js/waba_integration.js" 20 | 21 | # include custom scss in every website theme (without file extension ".scss") 22 | # website_theme_scss = "waba_integration/public/scss/website" 23 | 24 | # include js, css files in header of web form 25 | # webform_include_js = {"doctype": "public/js/doctype.js"} 26 | # webform_include_css = {"doctype": "public/css/doctype.css"} 27 | 28 | # include js in page 29 | # page_js = {"page" : "public/js/file.js"} 30 | 31 | # include js in doctype views 32 | # doctype_js = {"doctype" : "public/js/doctype.js"} 33 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 34 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 35 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 36 | 37 | # Home Pages 38 | # ---------- 39 | 40 | # application home page (will override Website Settings) 41 | # home_page = "login" 42 | 43 | # website user home page (by Role) 44 | # role_home_page = { 45 | # "Role": "home_page" 46 | # } 47 | 48 | # Generators 49 | # ---------- 50 | 51 | # automatically create page for each record of this doctype 52 | # website_generators = ["Web Page"] 53 | 54 | # Jinja 55 | # ---------- 56 | 57 | # add methods and filters to jinja environment 58 | # jinja = { 59 | # "methods": "waba_integration.utils.jinja_methods", 60 | # "filters": "waba_integration.utils.jinja_filters" 61 | # } 62 | 63 | # Installation 64 | # ------------ 65 | 66 | # before_install = "waba_integration.install.before_install" 67 | # after_install = "waba_integration.install.after_install" 68 | 69 | # Uninstallation 70 | # ------------ 71 | 72 | # before_uninstall = "waba_integration.uninstall.before_uninstall" 73 | # after_uninstall = "waba_integration.uninstall.after_uninstall" 74 | 75 | # Desk Notifications 76 | # ------------------ 77 | # See frappe.core.notifications.get_notification_config 78 | 79 | # notification_config = "waba_integration.notifications.get_notification_config" 80 | 81 | # Permissions 82 | # ----------- 83 | # Permissions evaluated in scripted ways 84 | 85 | # permission_query_conditions = { 86 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 87 | # } 88 | # 89 | # has_permission = { 90 | # "Event": "frappe.desk.doctype.event.event.has_permission", 91 | # } 92 | 93 | # DocType Class 94 | # --------------- 95 | # Override standard doctype classes 96 | 97 | # override_doctype_class = { 98 | # "ToDo": "custom_app.overrides.CustomToDo" 99 | # } 100 | 101 | # Document Events 102 | # --------------- 103 | # Hook on document methods and events 104 | 105 | # doc_events = { 106 | # "*": { 107 | # "on_update": "method", 108 | # "on_cancel": "method", 109 | # "on_trash": "method" 110 | # } 111 | # } 112 | 113 | # Scheduled Tasks 114 | # --------------- 115 | 116 | # scheduler_events = { 117 | # "all": [ 118 | # "waba_integration.tasks.all" 119 | # ], 120 | # "daily": [ 121 | # "waba_integration.tasks.daily" 122 | # ], 123 | # "hourly": [ 124 | # "waba_integration.tasks.hourly" 125 | # ], 126 | # "weekly": [ 127 | # "waba_integration.tasks.weekly" 128 | # ], 129 | # "monthly": [ 130 | # "waba_integration.tasks.monthly" 131 | # ], 132 | # } 133 | 134 | # Testing 135 | # ------- 136 | 137 | # before_tests = "waba_integration.install.before_tests" 138 | 139 | # Overriding Methods 140 | # ------------------------------ 141 | # 142 | # override_whitelisted_methods = { 143 | # "frappe.desk.doctype.event.event.get_events": "waba_integration.event.get_events" 144 | # } 145 | # 146 | # each overriding function accepts a `data` argument; 147 | # generated from the base implementation of the doctype dashboard, 148 | # along with any modifications made in other Frappe apps 149 | # override_doctype_dashboards = { 150 | # "Task": "waba_integration.task.get_dashboard_data" 151 | # } 152 | 153 | # exempt linked doctypes from being automatically cancelled 154 | # 155 | # auto_cancel_exempted_doctypes = ["Auto Repeat"] 156 | 157 | 158 | # User Data Protection 159 | # -------------------- 160 | 161 | # user_data_fields = [ 162 | # { 163 | # "doctype": "{doctype_1}", 164 | # "filter_by": "{filter_by}", 165 | # "redact_fields": ["{field_1}", "{field_2}"], 166 | # "partial": 1, 167 | # }, 168 | # { 169 | # "doctype": "{doctype_2}", 170 | # "filter_by": "{filter_by}", 171 | # "partial": 1, 172 | # }, 173 | # { 174 | # "doctype": "{doctype_3}", 175 | # "strict": False, 176 | # }, 177 | # { 178 | # "doctype": "{doctype_4}" 179 | # } 180 | # ] 181 | 182 | # Authentication and authorization 183 | # -------------------------------- 184 | 185 | # auth_hooks = [ 186 | # "waba_integration.auth.validate" 187 | # ] 188 | 189 | # Translation 190 | # -------------------------------- 191 | 192 | # Make link fields search translated document names for these DocTypes 193 | # Recommended only for DocTypes which have limited documents with untranslated names 194 | # For example: Role, Gender, etc. 195 | # translated_search_doctypes = [] 196 | -------------------------------------------------------------------------------- /waba_integration/modules.txt: -------------------------------------------------------------------------------- 1 | WhatsApp Business API Integration -------------------------------------------------------------------------------- /waba_integration/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/patches.txt -------------------------------------------------------------------------------- /waba_integration/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/public/.gitkeep -------------------------------------------------------------------------------- /waba_integration/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/templates/__init__.py -------------------------------------------------------------------------------- /waba_integration/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/templates/pages/__init__.py -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/whatsapp_business_api_integration/__init__.py -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/whatsapp_business_api_integration/doctype/__init__.py -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_message_status_update/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/whatsapp_business_api_integration/doctype/waba_message_status_update/__init__.py -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_message_status_update/test_waba_message_status_update.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestWABAMessageStatusUpdate(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_message_status_update/waba_message_status_update.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Hussain Nagaria and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('WABA Message Status Update', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_message_status_update/waba_message_status_update.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2022-05-22 13:01:51.293591", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "message", 10 | "message_id", 11 | "status", 12 | "timestamp", 13 | "recipient" 14 | ], 15 | "fields": [ 16 | { 17 | "fieldname": "message", 18 | "fieldtype": "Link", 19 | "label": "Message", 20 | "options": "WABA WhatsApp Message" 21 | }, 22 | { 23 | "fieldname": "status", 24 | "fieldtype": "Select", 25 | "label": "Status", 26 | "options": "Sent\nDelivered\nRead" 27 | }, 28 | { 29 | "fieldname": "timestamp", 30 | "fieldtype": "Data", 31 | "label": "Timestamp" 32 | }, 33 | { 34 | "fieldname": "recipient", 35 | "fieldtype": "Link", 36 | "label": "Recipient", 37 | "options": "WABA WhatsApp Contact" 38 | }, 39 | { 40 | "fieldname": "message_id", 41 | "fieldtype": "Data", 42 | "label": "Message ID" 43 | } 44 | ], 45 | "index_web_pages_for_search": 1, 46 | "links": [], 47 | "modified": "2022-05-22 13:21:56.047202", 48 | "modified_by": "Administrator", 49 | "module": "WhatsApp Business API Integration", 50 | "name": "WABA Message Status Update", 51 | "owner": "Administrator", 52 | "permissions": [ 53 | { 54 | "create": 1, 55 | "delete": 1, 56 | "email": 1, 57 | "export": 1, 58 | "print": 1, 59 | "read": 1, 60 | "report": 1, 61 | "role": "System Manager", 62 | "share": 1, 63 | "write": 1 64 | } 65 | ], 66 | "sort_field": "modified", 67 | "sort_order": "DESC", 68 | "states": [] 69 | } -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_message_status_update/waba_message_status_update.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | class WABAMessageStatusUpdate(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/whatsapp_business_api_integration/doctype/waba_settings/__init__.py -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_settings/test_waba_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestWABASettings(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_settings/waba_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Hussain Nagaria and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('WABA Settings', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_settings/waba_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2022-05-22 11:23:18.618233", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "access_token", 10 | "phone_number_id", 11 | "column_break_4", 12 | "business_account_id", 13 | "webhook_verify_token", 14 | "attachment_preferences_section", 15 | "automatically_download_images", 16 | "column_break_9", 17 | "automatically_download_audio" 18 | ], 19 | "fields": [ 20 | { 21 | "fieldname": "access_token", 22 | "fieldtype": "Password", 23 | "label": "Access Token", 24 | "length": 250 25 | }, 26 | { 27 | "fieldname": "phone_number_id", 28 | "fieldtype": "Data", 29 | "label": "Phone Number ID" 30 | }, 31 | { 32 | "fieldname": "business_account_id", 33 | "fieldtype": "Data", 34 | "label": "Business Account ID" 35 | }, 36 | { 37 | "fieldname": "webhook_verify_token", 38 | "fieldtype": "Data", 39 | "label": "Webhook Verify Token" 40 | }, 41 | { 42 | "default": "0", 43 | "fieldname": "automatically_download_images", 44 | "fieldtype": "Check", 45 | "label": "Automatically Download Images" 46 | }, 47 | { 48 | "fieldname": "column_break_4", 49 | "fieldtype": "Column Break" 50 | }, 51 | { 52 | "fieldname": "attachment_preferences_section", 53 | "fieldtype": "Section Break", 54 | "label": "Attachment Preferences" 55 | }, 56 | { 57 | "fieldname": "column_break_9", 58 | "fieldtype": "Column Break" 59 | }, 60 | { 61 | "default": "0", 62 | "fieldname": "automatically_download_audio", 63 | "fieldtype": "Check", 64 | "label": "Automatically Download Audio" 65 | } 66 | ], 67 | "index_web_pages_for_search": 1, 68 | "issingle": 1, 69 | "links": [], 70 | "modified": "2022-05-25 06:04:53.477667", 71 | "modified_by": "Administrator", 72 | "module": "WhatsApp Business API Integration", 73 | "name": "WABA Settings", 74 | "owner": "Administrator", 75 | "permissions": [ 76 | { 77 | "create": 1, 78 | "delete": 1, 79 | "email": 1, 80 | "print": 1, 81 | "read": 1, 82 | "role": "System Manager", 83 | "share": 1, 84 | "write": 1 85 | } 86 | ], 87 | "sort_field": "modified", 88 | "sort_order": "DESC", 89 | "states": [] 90 | } -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_settings/waba_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | class WABASettings(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_webhook_log/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/whatsapp_business_api_integration/doctype/waba_webhook_log/__init__.py -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_webhook_log/test_waba_webhook_log.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestWABAWebhookLog(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_webhook_log/waba_webhook_log.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Hussain Nagaria and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('WABA Webhook Log', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_webhook_log/waba_webhook_log.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2022-05-22 13:12:12.047672", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "payload" 10 | ], 11 | "fields": [ 12 | { 13 | "fieldname": "payload", 14 | "fieldtype": "JSON", 15 | "label": "Payload" 16 | } 17 | ], 18 | "index_web_pages_for_search": 1, 19 | "links": [], 20 | "modified": "2022-05-22 13:12:12.047672", 21 | "modified_by": "Administrator", 22 | "module": "WhatsApp Business API Integration", 23 | "name": "WABA Webhook Log", 24 | "owner": "Administrator", 25 | "permissions": [ 26 | { 27 | "create": 1, 28 | "delete": 1, 29 | "email": 1, 30 | "export": 1, 31 | "print": 1, 32 | "read": 1, 33 | "report": 1, 34 | "role": "System Manager", 35 | "share": 1, 36 | "write": 1 37 | } 38 | ], 39 | "sort_field": "modified", 40 | "sort_order": "DESC", 41 | "states": [] 42 | } -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_webhook_log/waba_webhook_log.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | class WABAWebhookLog(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_contact/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_contact/__init__.py -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_contact/test_waba_whatsapp_contact.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestWABAWhatsAppContact(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_contact/waba_whatsapp_contact.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Hussain Nagaria and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('WABA WhatsApp Contact', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_contact/waba_whatsapp_contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "field:whatsapp_id", 5 | "creation": "2022-05-22 13:04:30.760202", 6 | "doctype": "DocType", 7 | "editable_grid": 1, 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "whatsapp_id", 11 | "display_name" 12 | ], 13 | "fields": [ 14 | { 15 | "fieldname": "whatsapp_id", 16 | "fieldtype": "Data", 17 | "in_list_view": 1, 18 | "label": "WhatsApp ID", 19 | "reqd": 1, 20 | "unique": 1 21 | }, 22 | { 23 | "fieldname": "display_name", 24 | "fieldtype": "Data", 25 | "in_list_view": 1, 26 | "label": "Display Name" 27 | } 28 | ], 29 | "index_web_pages_for_search": 1, 30 | "links": [ 31 | { 32 | "group": "Received Messages", 33 | "link_doctype": "WABA WhatsApp Message", 34 | "link_fieldname": "from" 35 | }, 36 | { 37 | "group": "Sent Messages", 38 | "link_doctype": "WABA WhatsApp Message", 39 | "link_fieldname": "to" 40 | } 41 | ], 42 | "modified": "2022-05-26 23:42:54.548853", 43 | "modified_by": "Administrator", 44 | "module": "WhatsApp Business API Integration", 45 | "name": "WABA WhatsApp Contact", 46 | "naming_rule": "By fieldname", 47 | "owner": "Administrator", 48 | "permissions": [ 49 | { 50 | "create": 1, 51 | "delete": 1, 52 | "email": 1, 53 | "export": 1, 54 | "print": 1, 55 | "read": 1, 56 | "report": 1, 57 | "role": "System Manager", 58 | "share": 1, 59 | "write": 1 60 | } 61 | ], 62 | "quick_entry": 1, 63 | "sort_field": "modified", 64 | "sort_order": "DESC", 65 | "states": [], 66 | "title_field": "display_name" 67 | } -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_contact/waba_whatsapp_contact.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | class WABAWhatsAppContact(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_message/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_message/__init__.py -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_message/test_waba_whatsapp_message.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestWABAWhatsAppMessage(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_message/waba_whatsapp_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Hussain Nagaria and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("WABA WhatsApp Message", { 5 | refresh: function (frm) { 6 | if (!frm.doc.id) { 7 | const btn = frm.add_custom_button("Send Message", () => { 8 | frm 9 | .call({ 10 | doc: frm.doc, 11 | method: "send", 12 | btn, 13 | }) 14 | .then((m) => frm.refresh()); 15 | }); 16 | } 17 | 18 | if ( 19 | frm.doc.type === "Incoming" && 20 | ["Image", "Video", "Audio", "Document"].includes(frm.doc.message_type) && 21 | !frm.doc.media_file 22 | ) { 23 | const btn = frm.add_custom_button("Download Attachment File", () => { 24 | frm 25 | .call({ 26 | doc: frm.doc, 27 | method: "download_media", 28 | btn, 29 | }) 30 | .then((data) => { 31 | const file = data.message; 32 | frm.refresh(); 33 | frappe.msgprint({ 34 | title: "Attachment downloaded successfully.", 35 | message: `Attachment File: ${file.file_name}`, 36 | indicator: "green", 37 | }); 38 | }); 39 | }); 40 | } 41 | 42 | if (frm.doc.preview_html) { 43 | let wrapper = frm.get_field("preview_html_rendered").$wrapper; 44 | wrapper.html(frm.doc.preview_html); 45 | } 46 | 47 | if ( 48 | frm.doc.type === "Outgoing" && 49 | frm.doc.media_file && 50 | !frm.doc.media_uploaded 51 | ) { 52 | const btn = frm.add_custom_button("Upload Attachment File", () => { 53 | frm 54 | .call({ 55 | doc: frm.doc, 56 | method: "upload_media", 57 | btn, 58 | }) 59 | .then(() => { 60 | frm.refresh(); 61 | frappe.msgprint({ 62 | title: "Attachment uploaded successfully.", 63 | message: "You can send this message now!", 64 | indicator: "green", 65 | }); 66 | }); 67 | }); 68 | } 69 | 70 | if (frm.doc.type === "Incoming" && frm.doc.status !== "Marked As Seen") { 71 | const btn = frm.add_custom_button("Mark As Seen", () => { 72 | frm 73 | .call({ 74 | doc: frm.doc, 75 | method: "mark_as_seen", 76 | btn, 77 | }) 78 | .then(() => frm.refresh()); 79 | }); 80 | } 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_message/waba_whatsapp_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "hash", 4 | "creation": "2022-05-22 12:43:17.381824", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "image_preview", 10 | "preview_html_rendered", 11 | "preview_html", 12 | "section_break_3", 13 | "status", 14 | "from", 15 | "to", 16 | "message_type", 17 | "column_break_3", 18 | "type", 19 | "id", 20 | "message_body", 21 | "media_information_section", 22 | "media_id", 23 | "media_hash", 24 | "media_mime_type", 25 | "column_break_9", 26 | "media_filename", 27 | "media_caption", 28 | "media_file", 29 | "media_image", 30 | "media_uploaded" 31 | ], 32 | "fields": [ 33 | { 34 | "default": "Pending", 35 | "fieldname": "status", 36 | "fieldtype": "Select", 37 | "in_list_view": 1, 38 | "label": "Status", 39 | "options": "Pending\nSent\nDelivered\nRead\nReceived\nMarked As Seen" 40 | }, 41 | { 42 | "default": "Outgoing", 43 | "fieldname": "type", 44 | "fieldtype": "Select", 45 | "label": "Type", 46 | "options": "Incoming\nOutgoing", 47 | "read_only": 1 48 | }, 49 | { 50 | "fieldname": "id", 51 | "fieldtype": "Data", 52 | "in_list_view": 1, 53 | "label": "Message ID", 54 | "read_only": 1, 55 | "unique": 1 56 | }, 57 | { 58 | "depends_on": "eval:doc.type===\"Incoming\"", 59 | "fieldname": "from", 60 | "fieldtype": "Link", 61 | "label": "From", 62 | "options": "WABA WhatsApp Contact" 63 | }, 64 | { 65 | "fieldname": "media_id", 66 | "fieldtype": "Data", 67 | "label": "Media ID", 68 | "read_only": 1 69 | }, 70 | { 71 | "fieldname": "media_hash", 72 | "fieldtype": "Data", 73 | "label": "Media Hash", 74 | "read_only": 1 75 | }, 76 | { 77 | "fieldname": "media_mime_type", 78 | "fieldtype": "Data", 79 | "label": "Media MIME Type" 80 | }, 81 | { 82 | "fieldname": "media_filename", 83 | "fieldtype": "Data", 84 | "label": "Media Filename" 85 | }, 86 | { 87 | "fieldname": "media_caption", 88 | "fieldtype": "Data", 89 | "label": "Media Caption" 90 | }, 91 | { 92 | "depends_on": "eval:doc.message_type!=\"Text\"", 93 | "fieldname": "media_information_section", 94 | "fieldtype": "Section Break", 95 | "label": "Media Information" 96 | }, 97 | { 98 | "fieldname": "column_break_9", 99 | "fieldtype": "Column Break" 100 | }, 101 | { 102 | "fieldname": "column_break_3", 103 | "fieldtype": "Column Break" 104 | }, 105 | { 106 | "fieldname": "message_type", 107 | "fieldtype": "Select", 108 | "in_list_view": 1, 109 | "label": "Message Type", 110 | "options": "Text\nImage\nAudio\nVideo\nSystem\nDocument" 111 | }, 112 | { 113 | "depends_on": "eval:doc.message_type===\"Text\"", 114 | "fieldname": "message_body", 115 | "fieldtype": "Markdown Editor", 116 | "label": "Message Body" 117 | }, 118 | { 119 | "depends_on": "eval:doc.type===\"Outgoing\"", 120 | "fieldname": "to", 121 | "fieldtype": "Link", 122 | "label": "To", 123 | "options": "WABA WhatsApp Contact" 124 | }, 125 | { 126 | "depends_on": "eval:!(doc.type==\"Outgoing\"&&doc.message_type==\"Image\")", 127 | "fieldname": "media_file", 128 | "fieldtype": "Attach", 129 | "label": "Media File" 130 | }, 131 | { 132 | "depends_on": "eval:(doc.message_type==\"Image\"&&doc.media_file)", 133 | "fieldname": "image_preview", 134 | "fieldtype": "Image", 135 | "label": "Image Preview", 136 | "options": "media_image" 137 | }, 138 | { 139 | "depends_on": "eval:doc.type==\"Outgoing\"&&doc.message_type==\"Image\"", 140 | "fieldname": "media_image", 141 | "fieldtype": "Attach Image", 142 | "label": "Media Image" 143 | }, 144 | { 145 | "fieldname": "section_break_3", 146 | "fieldtype": "Section Break" 147 | }, 148 | { 149 | "fieldname": "preview_html", 150 | "fieldtype": "HTML Editor", 151 | "hidden": 1, 152 | "label": "Preview HTML" 153 | }, 154 | { 155 | "depends_on": "eval:doc.message_type==\"Audio\"||doc.message_type==\"Video\"", 156 | "fieldname": "preview_html_rendered", 157 | "fieldtype": "HTML", 158 | "label": "Preview HTML Rendered" 159 | }, 160 | { 161 | "default": "0", 162 | "fieldname": "media_uploaded", 163 | "fieldtype": "Check", 164 | "label": "Media Uploaded", 165 | "read_only": 1 166 | } 167 | ], 168 | "image_field": "media_image", 169 | "index_web_pages_for_search": 1, 170 | "links": [], 171 | "modified": "2022-05-26 23:52:27.287967", 172 | "modified_by": "Administrator", 173 | "module": "WhatsApp Business API Integration", 174 | "name": "WABA WhatsApp Message", 175 | "naming_rule": "Random", 176 | "owner": "Administrator", 177 | "permissions": [ 178 | { 179 | "create": 1, 180 | "delete": 1, 181 | "email": 1, 182 | "export": 1, 183 | "print": 1, 184 | "read": 1, 185 | "report": 1, 186 | "role": "System Manager", 187 | "share": 1, 188 | "write": 1 189 | } 190 | ], 191 | "sort_field": "modified", 192 | "sort_order": "DESC", 193 | "states": [] 194 | } -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/doctype/waba_whatsapp_message/waba_whatsapp_message.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Hussain Nagaria and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | import requests 6 | import mimetypes 7 | 8 | from typing import Dict 9 | from functools import lru_cache 10 | from frappe.model.document import Document 11 | 12 | 13 | MEDIA_TYPES = ("image", "sticker", "document", "audio", "video") 14 | 15 | 16 | class WABAWhatsAppMessage(Document): 17 | def validate(self): 18 | self.validate_image_attachment() 19 | 20 | if self.message_type == "Audio" and self.media_file: 21 | self.preview_html = f""" 22 | 26 | """ 27 | 28 | if self.message_type == "Video" and self.media_file: 29 | self.preview_html = f""" 30 | 34 | """ 35 | 36 | def validate_image_attachment(self): 37 | if self.media_image: 38 | self.media_file = self.media_image 39 | 40 | if self.media_file and self.message_type == "Image": 41 | self.media_image = self.media_file 42 | 43 | @frappe.whitelist() 44 | def send(self) -> Dict: 45 | if not self.to: 46 | frappe.throw("Recepient (`to`) is required to send message.") 47 | 48 | access_token = frappe.utils.password.get_decrypted_password( 49 | "WABA Settings", "WABA Settings", "access_token" 50 | ) 51 | api_base = "https://graph.facebook.com/v13.0" 52 | phone_number_id = frappe.db.get_single_value("WABA Settings", "phone_number_id") 53 | 54 | endpoint = f"{api_base}/{phone_number_id}/messages" 55 | 56 | response_data = { 57 | "messaging_product": "whatsapp", 58 | "recipient_type": "individual", 59 | "to": self.to, 60 | "type": self.message_type.lower(), 61 | } 62 | 63 | if self.message_type == "Text": 64 | response_data["text"] = {"preview_url": False, "body": self.message_body} 65 | 66 | if self.message_type in ("Audio", "Image", "Video", "Document"): 67 | if not self.media_id: 68 | frappe.throw("Please attach and upload the media before sending this message.") 69 | 70 | response_data[self.message_type.lower()] = { 71 | "id": self.media_id, 72 | } 73 | 74 | if self.message_type == "Document": 75 | response_data[self.message_type.lower()]["filename"] = self.media_filename 76 | response_data[self.message_type.lower()]["caption"] = self.media_caption 77 | 78 | response = requests.post( 79 | endpoint, 80 | json=response_data, 81 | headers={ 82 | "Authorization": "Bearer " + access_token, 83 | "Content-Type": "application/json", 84 | }, 85 | ) 86 | 87 | if response.ok: 88 | self.id = response.json().get("messages")[0]["id"] 89 | self.status = "Sent" 90 | self.save(ignore_permissions=True) 91 | return response.json() 92 | else: 93 | frappe.throw(response.json().get("error").get("message")) 94 | 95 | @frappe.whitelist() 96 | def download_media(self) -> Dict: 97 | url = self.get_media_url() 98 | access_token = self.get_access_token() 99 | response = requests.get( 100 | url, 101 | headers={ 102 | "Authorization": "Bearer " + access_token, 103 | }, 104 | ) 105 | 106 | file_name = get_media_extention(self, response.headers.get("Content-Type")) 107 | file_doc = frappe.get_doc( 108 | { 109 | "doctype": "File", 110 | "file_name": file_name, 111 | "content": response.content, 112 | "attached_to_doctype": "WABA WhatsApp Message", 113 | "attached_to_name": self.name, 114 | "attached_to_field": "media_file", 115 | } 116 | ).insert(ignore_permissions=True) 117 | 118 | self.set("media_file", file_doc.file_url) 119 | 120 | # Will be used to display image preview 121 | if self.message_type == "Image": 122 | self.set("media_image", file_doc.file_url) 123 | 124 | self.save() 125 | 126 | return file_doc.as_dict() 127 | 128 | def get_media_url(self) -> str: 129 | if not self.media_id: 130 | frappe.throw("`media_id` is missing.") 131 | 132 | api_base = "https://graph.facebook.com/v13.0" 133 | access_token = self.get_access_token() 134 | response = requests.get( 135 | f"{api_base}/{self.media_id}", 136 | headers={ 137 | "Authorization": "Bearer " + access_token, 138 | }, 139 | ) 140 | 141 | if not response.ok: 142 | frappe.throw("Error fetching media URL") 143 | 144 | return response.json().get("url") 145 | 146 | @lru_cache 147 | def get_access_token(self) -> str: 148 | return frappe.utils.password.get_decrypted_password( 149 | "WABA Settings", "WABA Settings", "access_token" 150 | ) 151 | 152 | @frappe.whitelist() 153 | def upload_media(self): 154 | if not self.media_file: 155 | frappe.throw("`media_file` is required to upload media.") 156 | 157 | media_file_path = frappe.get_doc( 158 | "File", {"file_url": self.media_file} 159 | ).get_full_path() 160 | access_token = self.get_access_token() 161 | api_base = "https://graph.facebook.com/v13.0" 162 | phone_number_id = frappe.db.get_single_value("WABA Settings", "phone_number_id") 163 | 164 | if not self.media_mime_type: 165 | self.media_mime_type = mimetypes.guess_type(self.media_file)[0] 166 | 167 | # Way to send multi-part form data 168 | # Ref: https://stackoverflow.com/a/35974071 169 | form_data = { 170 | "file": ( 171 | "file", 172 | open(media_file_path, "rb"), 173 | self.media_mime_type, 174 | ), 175 | "messaging_product": (None, "whatsapp"), 176 | "type": (None, self.media_mime_type), 177 | } 178 | response = requests.post( 179 | f"{api_base}/{phone_number_id}/media", 180 | files=form_data, 181 | headers={ 182 | "Authorization": "Bearer " + access_token, 183 | }, 184 | ) 185 | 186 | if response.ok: 187 | self.media_id = response.json().get("id") 188 | self.media_uploaded = True 189 | self.save(ignore_permissions=True) 190 | else: 191 | frappe.throw(response.json().get("error").get("message")) 192 | 193 | @frappe.whitelist() 194 | def mark_as_seen(self): 195 | if self.type != "Incoming": 196 | frappe.throw("Only incoming messages can be marked as seen.") 197 | 198 | phone_number_id = frappe.db.get_single_value("WABA Settings", "phone_number_id") 199 | endpoint = f"https://graph.facebook.com/v13.0/{phone_number_id}/messages" 200 | access_token = self.get_access_token() 201 | 202 | response = requests.post( 203 | endpoint, 204 | json={"messaging_product": "whatsapp", "status": "read", "message_id": self.id}, 205 | headers={ 206 | "Authorization": "Bearer " + access_token, 207 | "Content-Type": "application/json", 208 | }, 209 | ) 210 | 211 | if response.ok: 212 | self.status = "Marked As Seen" 213 | self.save() 214 | else: 215 | frappe.throw(response.json().get("error").get("message")) 216 | 217 | 218 | def create_waba_whatsapp_message(message: Dict) -> WABAWhatsAppMessage: 219 | message_type = message.get("type") 220 | 221 | # Create whatsapp contact doc if not exists 222 | received_from = message.get("from") 223 | if not frappe.db.exists("WABA WhatsApp Contact", {"name": received_from}): 224 | frappe.get_doc( 225 | { 226 | "doctype": "WABA WhatsApp Contact", 227 | "whatsapp_id": received_from, 228 | } 229 | ).insert(ignore_permissions=True) 230 | 231 | message_data = frappe._dict( 232 | { 233 | "doctype": "WABA WhatsApp Message", 234 | "type": "Incoming", 235 | "status": "Received", 236 | "from": message.get("from"), 237 | "id": message.get("id"), 238 | "message_type": message_type.title(), 239 | } 240 | ) 241 | 242 | if message_type == "text": 243 | message_data["message_body"] = message.get("text").get("body") 244 | elif message_type in MEDIA_TYPES: 245 | message_data["media_id"] = message.get(message_type).get("id") 246 | message_data["media_mime_type"] = message.get(message_type).get("mime_type") 247 | message_data["media_hash"] = message.get(message_type).get("sha256") 248 | 249 | if message_type == "document": 250 | message_data["media_filename"] = message.get("document").get("filename") 251 | message_data["media_caption"] = message.get("document").get("caption") 252 | 253 | message_doc = frappe.get_doc(message_data).insert(ignore_permissions=True) 254 | 255 | wants_automatic_image_downloads = frappe.db.get_single_value( 256 | "WABA Settings", "automatically_download_images" 257 | ) 258 | 259 | wants_automatic_audio_downloads = frappe.db.get_single_value( 260 | "WABA Settings", "automatically_download_audio" 261 | ) 262 | if (message_doc.message_type == "Image" and wants_automatic_image_downloads) or ( 263 | message_doc.message_type == "Audio" and wants_automatic_audio_downloads 264 | ): 265 | try: 266 | current_user = frappe.session.user 267 | frappe.set_user("Administrator") 268 | message_doc.download_media() 269 | frappe.set_user(current_user) 270 | except: 271 | frappe.log_error( 272 | f"WABA: Problem downloading {message_doc.message_type}", frappe.get_traceback() 273 | ) 274 | 275 | return message_doc 276 | 277 | 278 | def process_status_update(status: Dict): 279 | message_id = status.get("id") 280 | status = status.get("status") 281 | 282 | frappe.db.set_value( 283 | "WABA WhatsApp Message", {"id": message_id}, "status", status.title() 284 | ) 285 | 286 | 287 | def get_media_extention(message_doc: WABAWhatsAppMessage, content_type: str) -> str: 288 | return message_doc.media_filename or ( 289 | "attachment_." + content_type.split(";")[0].split("/")[1] 290 | ) 291 | -------------------------------------------------------------------------------- /waba_integration/whatsapp_business_api_integration/workspace/whatsapp/whatsapp.json: -------------------------------------------------------------------------------- 1 | { 2 | "charts": [], 3 | "content": "[{\"id\":\"wc3HHqsg-p\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"WABA Settings\",\"col\":3}},{\"id\":\"Y_1bDAzZbc\",\"type\":\"card\",\"data\":{\"card_name\":\"Communication\",\"col\":4}},{\"id\":\"9HACpwJXJq\",\"type\":\"card\",\"data\":{\"card_name\":\"Process Tracking\",\"col\":4}}]", 4 | "creation": "2023-05-14 12:41:13.182006", 5 | "docstatus": 0, 6 | "doctype": "Workspace", 7 | "for_user": "", 8 | "hide_custom": 0, 9 | "icon": "comment", 10 | "idx": 0, 11 | "is_hidden": 0, 12 | "label": "WhatsApp", 13 | "links": [ 14 | { 15 | "hidden": 0, 16 | "is_query_report": 0, 17 | "label": "Communication", 18 | "link_count": 2, 19 | "onboard": 0, 20 | "type": "Card Break" 21 | }, 22 | { 23 | "hidden": 0, 24 | "is_query_report": 0, 25 | "label": "WABA WhatsApp Contact", 26 | "link_count": 0, 27 | "link_to": "WABA WhatsApp Contact", 28 | "link_type": "DocType", 29 | "onboard": 0, 30 | "type": "Link" 31 | }, 32 | { 33 | "hidden": 0, 34 | "is_query_report": 0, 35 | "label": "WABA WhatsApp Message", 36 | "link_count": 0, 37 | "link_to": "WABA WhatsApp Message", 38 | "link_type": "DocType", 39 | "onboard": 0, 40 | "type": "Link" 41 | }, 42 | { 43 | "hidden": 0, 44 | "is_query_report": 0, 45 | "label": "Process Tracking", 46 | "link_count": 2, 47 | "onboard": 0, 48 | "type": "Card Break" 49 | }, 50 | { 51 | "hidden": 0, 52 | "is_query_report": 0, 53 | "label": "WABA Message Status Update", 54 | "link_count": 0, 55 | "link_to": "WABA Message Status Update", 56 | "link_type": "DocType", 57 | "onboard": 0, 58 | "type": "Link" 59 | }, 60 | { 61 | "hidden": 0, 62 | "is_query_report": 0, 63 | "label": "WABA Webhook Log", 64 | "link_count": 0, 65 | "link_to": "WABA Webhook Log", 66 | "link_type": "DocType", 67 | "onboard": 0, 68 | "type": "Link" 69 | } 70 | ], 71 | "modified": "2023-05-14 12:51:35.930945", 72 | "modified_by": "Administrator", 73 | "module": "WhatsApp Business API Integration", 74 | "name": "WhatsApp", 75 | "number_cards": [], 76 | "owner": "Administrator", 77 | "parent_page": "", 78 | "public": 1, 79 | "quick_lists": [], 80 | "roles": [], 81 | "sequence_id": 2.0, 82 | "shortcuts": [ 83 | { 84 | "doc_view": "", 85 | "label": "WABA Settings", 86 | "link_to": "WABA Settings", 87 | "type": "DocType" 88 | } 89 | ], 90 | "title": "WhatsApp" 91 | } -------------------------------------------------------------------------------- /waba_integration/www/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/waba_integration/5fd0f430efb8001d7080228539f220ee92a56c63/waba_integration/www/__init__.py --------------------------------------------------------------------------------