odoo18/addons_extensions/whatsapp/tools/whatsapp_api.py

268 lines
14 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import requests
import threading
import json
from odoo import _
from odoo.exceptions import RedirectWarning
from odoo.addons.whatsapp.tools.whatsapp_exception import WhatsAppError
_logger = logging.getLogger(__name__)
DEFAULT_ENDPOINT = "https://graph.facebook.com/v17.0"
class WhatsAppApi:
def __init__(self, wa_account_id):
wa_account_id.ensure_one()
self.wa_account_id = wa_account_id
self.phone_uid = wa_account_id.phone_uid
self.token = wa_account_id.sudo().token
self.is_shared_account = False
def __api_requests(self, request_type, url, auth_type="", params=False, headers=None, data=False, files=False, endpoint_include=False):
if getattr(threading.current_thread(), 'testing', False):
raise WhatsAppError("API requests disabled in testing.")
headers = headers or {}
params = params or {}
if not all([self.token, self.phone_uid]):
action = self.wa_account_id.env.ref('whatsapp.whatsapp_account_action')
raise RedirectWarning(_("To use WhatsApp Configure it first"), action=action.id, button_text=_("Configure Whatsapp Business Account"))
if auth_type == 'oauth':
headers.update({'Authorization': f'OAuth {self.token}'})
if auth_type == 'bearer':
headers.update({'Authorization': f'Bearer {self.token}'})
call_url = (DEFAULT_ENDPOINT + url) if not endpoint_include else url
try:
res = requests.request(request_type, call_url, params=params, headers=headers, data=data, files=files, timeout=10)
except requests.exceptions.RequestException:
raise WhatsAppError(failure_type='network')
# raise if json-parseable and 'error' in json
try:
if 'error' in res.json():
raise WhatsAppError(*self._prepare_error_response(res.json()))
except ValueError:
if not res.ok:
raise WhatsAppError(failure_type='network')
return res
def _prepare_error_response(self, response):
"""
This method is used to prepare error response
:return tuple[str, int]: (error_message, whatsapp_error_code | -1)
"""
if response.get('error'):
error = response['error']
desc = error.get('message', '')
desc += (' - ' + error['error_user_title']) if error.get('error_user_title') else ''
desc += ('\n\n' + error['error_user_msg']) if error.get('error_user_msg') else ''
code = error.get('code', 'odoo')
return (desc if desc else _("Non-descript Error"), code)
return (_("Something went wrong when contacting WhatsApp, please try again later. If this happens frequently, contact support."), -1)
def _get_all_template(self, fetch_all=False):
"""
This method is used to get all the template from the WhatsApp Business Account
API Documentation: https://developers.facebook.com/docs/graph-api/reference/whats-app-business-account/message_templates
"""
if self.is_shared_account:
raise WhatsAppError(failure_type='account')
template_url = f"/{self.wa_account_id.account_uid}/message_templates?fields=name,components,language,status,category,id,quality_score"
_logger.info("Sync templates for account %s [%s]", self.wa_account_id.name, self.wa_account_id.id)
if fetch_all:
final_response_json = {}
# Fetch 200 templates at once
template_url += "&limit=200"
endpoint_include = False
while template_url:
response = self.__api_requests("GET", url=template_url, auth_type="bearer", endpoint_include=endpoint_include)
response_json = response.json()
if final_response_json:
# Add fetched data to existing response
response_data = response_json.get("data", [])
final_response_json.setdefault("data", []).extend(response_data)
else:
final_response_json = response_json
# Fetch the next URL if it exists in response to fetch more templates
template_url = response_json.get("paging", {}).get("next")
endpoint_include = bool(template_url)
else:
response = self.__api_requests("GET", url=template_url, auth_type="bearer")
final_response_json = response.json()
return final_response_json
def _get_template_data(self, wa_template_uid):
"""
This method is used to get one template details using template uid from the WhatsApp Business Account
API Documentation: https://developers.facebook.com/docs/graph-api/reference/whats-app-business-account/message_templates
"""
if self.is_shared_account:
raise WhatsAppError(failure_type='account')
_logger.info("Get template details for template uid %s using account %s [%s]", wa_template_uid, self.wa_account_id.name, self.wa_account_id.id)
response = self.__api_requests("GET", f"/{wa_template_uid}?fields=name,components,language,status,category,id,quality_score", auth_type="bearer")
return response.json()
def _upload_demo_document(self, attachment):
"""
This method is used to get a handle to later upload a demo document.
Only use for template registration.
API documentation https://developers.facebook.com/docs/graph-api/guides/upload
"""
if self.is_shared_account:
raise WhatsAppError(failure_type='account')
# Open session
app_uid = self.wa_account_id.app_uid
params = {
'file_length': attachment.file_size,
'file_type': attachment.mimetype,
'access_token': self.token,
}
_logger.info("Open template sample document upload session with file size %s Bites of mimetype %s on account %s [%s]", attachment.file_size, attachment.mimetype, self.wa_account_id.name, self.wa_account_id.id)
uploads_session_response = self.__api_requests("POST", f"/{app_uid}/uploads", params=params)
uploads_session_response_json = uploads_session_response.json()
upload_session_id = uploads_session_response_json.get('id')
if not upload_session_id:
raise WhatsAppError(_("Document upload session open failed, please retry after sometime."))
# Upload file
_logger.info("Upload sample document on the opened session using account %s [%s]", self.wa_account_id.name, self.wa_account_id.id)
upload_file_response = self.__api_requests("POST", f"/{upload_session_id}", params=params, auth_type="oauth", headers={'file_offset': '0'}, data=attachment.raw)
upload_file_response_json = upload_file_response.json()
file_handle = upload_file_response_json.get('h')
if not file_handle:
raise WhatsAppError(_("Document upload failed, please retry after sometime."))
return file_handle
def _submit_template_new(self, json_data):
"""
This method is used to submit template for approval
If template was submitted before, we have wa_template_uid and we call template update URL
API Documentation: https://developers.facebook.com/docs/graph-api/reference/whats-app-business-account/message_templates#Creating
"""
if self.is_shared_account:
raise WhatsAppError(failure_type='account')
_logger.info("Submit new template for account %s [%s]", self.wa_account_id.name, self.wa_account_id.id)
response = self.__api_requests("POST", f"/{self.wa_account_id.account_uid}/message_templates",
auth_type="bearer", headers={'Content-Type': 'application/json'}, data=json_data)
response_json = response.json()
if response_json.get('id'):
return {'id': response_json['id'], 'status': response_json['status']}
raise WhatsAppError(*self._prepare_error_response(response_json))
def _submit_template_update(self, json_data, wa_template_uid):
if self.is_shared_account:
raise WhatsAppError(failure_type='account')
_logger.info("Update template : %s for account %s [%s]", wa_template_uid, self.wa_account_id.name, self.wa_account_id.id)
response = self.__api_requests("POST", f"/{wa_template_uid}",
auth_type="bearer", headers={'Content-Type': 'application/json'}, data=json_data)
response_json = response.json()
if response_json.get('success'):
return True
raise WhatsAppError(*self._prepare_error_response(response_json))
def _send_whatsapp(self, number, message_type, send_vals, parent_message_id=False):
""" Send WA messages for all message type using WhatsApp Business Account
API Documentation:
Normal - https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages
Template send - https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates
"""
data = {
'messaging_product': 'whatsapp',
'recipient_type': 'individual',
'to': number
}
# if there is parent_message_id then we send message as reply
if parent_message_id:
data.update({
'context': {
'message_id': parent_message_id
},
})
if message_type in ('template', 'text', 'document', 'image', 'audio', 'video'):
data.update({
'type': message_type,
message_type: send_vals
})
json_data = json.dumps(data)
_logger.info("Send %s message from account %s [%s]", message_type, self.wa_account_id.name, self.wa_account_id.id)
response = self.__api_requests(
"POST",
f"/{self.phone_uid}/messages",
auth_type="bearer",
headers={'Content-Type': 'application/json'},
data=json_data
)
response_json = response.json()
if response_json.get('messages'):
msg_uid = response_json['messages'][0]['id']
return msg_uid
raise WhatsAppError(*self._prepare_error_response(response_json))
def _get_header_data_from_handle(self, url):
""" This method is used to get template demo document from url """
_logger.info("Get header data for url %s from account %s [%s]", url, self.wa_account_id.name, self.wa_account_id.id)
response = self.__api_requests("GET", url, endpoint_include=True)
mimetype = requests.head(url, timeout=5).headers.get('Content-Type')
data = response.content
return data, mimetype
def _get_whatsapp_document(self, document_id):
"""
This method is used to get document from WhatsApp sent by user
API Documentation: https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media
"""
_logger.info("Get document url for document uid %s from account %s [%s]", document_id, self.wa_account_id.name, self.wa_account_id.id)
response = self.__api_requests("GET", f"/{document_id}", auth_type="bearer")
response_json = response.json()
file_url = response_json.get('url')
_logger.info("Get document from url for account %s [%s]", self.wa_account_id.name, self.wa_account_id.id)
file_response = self.__api_requests("GET", file_url, auth_type="bearer", endpoint_include=True)
return file_response.content
def _upload_whatsapp_document(self, attachment):
"""
This method is used to upload document for sending via WhatsApp
API Documentation: https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media
"""
payload = {'messaging_product': 'whatsapp'}
files = [('file', (attachment.name, attachment.raw, attachment.mimetype))]
_logger.info("Upload document of mimetype %s for phone uid %s", attachment.mimetype, self.phone_uid)
response = self.__api_requests("POST", f"/{self.phone_uid}/media", auth_type='bearer', data=payload, files=files)
response_json = response.json()
if response_json.get('id'):
return response_json['id']
raise WhatsAppError(*self._prepare_error_response(response_json))
def _test_connection(self):
""" This method is used to test connection of WhatsApp Business Account"""
_logger.info("Test connection: Verify set phone uid is available in account %s [%s]", self.wa_account_id.name, self.wa_account_id.id)
response = self.__api_requests("GET", f"/{self.wa_account_id.account_uid}/phone_numbers", auth_type='bearer')
data = response.json().get('data', [])
phone_values = [phone['id'] for phone in data if 'id' in phone]
if self.wa_account_id.phone_uid not in phone_values:
raise WhatsAppError(_("Phone number Id is wrong."), 'account')
_logger.info("Test connection: check app uid and token set in account %s [%s]", self.wa_account_id.name, self.wa_account_id.id)
uploads_session_response = self.__api_requests("POST", f"/{self.wa_account_id.app_uid}/uploads", params={'access_token': self.token})
upload_session_id = uploads_session_response.json().get('id')
if not upload_session_id:
raise WhatsAppError(*self._prepare_error_response(uploads_session_response.json()))
return