174 lines
8.2 KiB
Python

# -*- coding: utf-8 -*-
import logging
import json
from odoo import http, fields, _
from odoo.http import request, Response
_logger = logging.getLogger(__name__)
class WhatsAppWebhookController(http.Controller):
@http.route('/social/whatsapp/webhook', type='http', auth='public', methods=['GET', 'POST'], csrf=False)
def whatsapp_webhook(self, **kwargs):
"""
Secure WhatsApp Cloud API Webhook endpoint:
- GET: Handles Meta verification challenge
- POST: Handles real-time message events and status updates
"""
if request.httprequest.method == 'GET':
return self._handle_verification(kwargs)
elif request.httprequest.method == 'POST':
return self._handle_incoming_event()
return Response("Method Not Allowed", status=405)
def _handle_verification(self, kwargs):
"""Verifies Meta webhook challenge during setup."""
mode = kwargs.get('hub.mode') or request.params.get('hub.mode')
token = kwargs.get('hub.verify_token') or request.params.get('hub.verify_token')
challenge = kwargs.get('hub.challenge') or request.params.get('hub.challenge')
if mode == 'subscribe' and token and challenge:
# Check against any company's configured verify token or system default
companies = request.env['res.company'].sudo().search([])
valid = any(comp.whatsapp_verify_token == token for comp in companies)
# Also check system parameter fallback
if not valid:
sys_token = request.env['ir.config_parameter'].sudo().get_param('dine360.whatsapp_verify_token')
valid = (token == sys_token)
if valid:
_logger.info("WhatsApp webhook challenge verified successfully.")
matching_company = companies.filtered(lambda c: c.whatsapp_verify_token == token)
if matching_company:
matching_company[:1].write({
'whatsapp_webhook_verified': True,
'whatsapp_last_webhook_ping': fields.Datetime.now(),
})
return request.make_response(str(challenge), [('Content-Type', 'text/plain')], status=200)
else:
_logger.warning("WhatsApp webhook verification token mismatch. Received: %s", token)
return Response("Forbidden: Invalid verify token", status=403)
_logger.warning("Invalid WhatsApp webhook GET request parameters: %s", kwargs)
return Response("Bad Request", status=400)
def _handle_incoming_event(self):
"""Processes incoming events from Meta (messages, status updates)."""
raw_body = request.httprequest.get_data()
signature_header = request.httprequest.headers.get('X-Hub-Signature-256')
if not raw_body:
return Response("Empty Body", status=400)
try:
payload = json.loads(raw_body.decode('utf-8'))
except Exception as e:
_logger.error("Failed to parse JSON in WhatsApp webhook: %s", e)
return Response("Invalid JSON", status=400)
# Optional HMAC-SHA256 signature verification if Meta App Secret is set
wa_api = request.env['social.whatsapp.api'].sudo()
companies = request.env['res.company'].sudo().search([('whatsapp_enabled', '=', True)])
if signature_header:
app_secret = companies.filtered(lambda c: c.meta_app_secret)[:1].meta_app_secret
if app_secret and not wa_api.verify_webhook_signature(raw_body, signature_header, app_secret):
_logger.warning("X-Hub-Signature-256 validation failed for WhatsApp webhook.")
return Response("Forbidden: Signature mismatch", status=403)
# Process entries
try:
entries = payload.get('entry', [])
for entry in entries:
changes = entry.get('changes', [])
for change in changes:
value = change.get('value', {})
metadata = value.get('metadata', {})
phone_number_id = metadata.get('phone_number_id')
# Identify corresponding company
company = companies.filtered(lambda c: c.whatsapp_phone_number_id == phone_number_id)[:1]
if not company:
company = request.env.company
# 1. Process delivery status receipts (sent -> delivered -> read)
statuses = value.get('statuses', [])
for status in statuses:
self._process_message_status(status, company)
# 2. Process incoming customer messages
messages = value.get('messages', [])
for msg in messages:
self._process_customer_message(msg, company, raw_body.decode('utf-8'))
# Mark webhook ping timestamp
if companies:
companies[:1].write({'whatsapp_last_webhook_ping': fields.Datetime.now()})
except Exception as e:
_logger.exception("Unexpected error processing WhatsApp webhook event: %s", e)
# Meta requires an immediate 200 OK
return Response("EVENT_RECEIVED", status=200)
def _process_message_status(self, status_data, company):
"""Updates WhatsApp notification state when Meta delivers status callbacks."""
wamid = status_data.get('id')
new_status = status_data.get('status') # delivered, read, failed, sent
if not wamid or not new_status:
return
notif_model = request.env['social.whatsapp.notification'].sudo()
notif = notif_model.search([('meta_message_id', '=', wamid)], limit=1)
if notif:
state_map = {
'sent': 'sent',
'delivered': 'delivered',
'read': 'read',
'failed': 'failed',
}
if new_status in state_map:
vals = {'state': state_map[new_status]}
if new_status == 'failed':
errors = status_data.get('errors', [])
if errors:
vals['error_message'] = errors[0].get('message', 'Delivery failed')
notif.write(vals)
_logger.info("Updated WhatsApp notification %s status to '%s'", notif.id, new_status)
def _process_customer_message(self, msg_data, company, raw_payload_str):
"""Logs customer inquiry and triggers automated status response if order is found."""
msg_model = request.env['social.whatsapp.message'].sudo()
record = msg_model.process_incoming_message(msg_data, company=company)
if not record:
return
# Store raw payload for audit
record.write({'raw_payload': raw_payload_str})
# Automated Order Status Response if customer is asking about an identifiable order
if record.sale_order_id and record.body:
body_lower = record.body.lower()
order_keywords = ['where is my order', 'order status', 'track', 'status of my order', 'when will my order arrive']
if any(kw in body_lower for kw in order_keywords):
order = record.sale_order_id
state_labels = {
'draft': 'Quotation created',
'sent': 'Quotation sent',
'sale': 'Confirmed & In Preparation',
'done': 'Completed',
'cancel': 'Cancelled',
}
order_state = state_labels.get(order.state, order.state)
reply_text = _("Hello! Your order %(order)s is currently %(state)s. Thank you for your patience!") % {
'order': order.name,
'state': order_state
}
# Send reply within 24h window
wa_api = request.env['social.whatsapp.api'].sudo()
res = wa_api.send_text_message(company, record.from_phone, reply_text)
if res.get('success'):
record.write({'status': 'replied'})
_logger.info("Automated order status reply sent to %s for order %s", record.from_phone, order.name)