# -*- 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)