Browse Source

FIX: Prevent WhatsApp infinite loop and correct self-message attribution

M22 Bot 2 weeks ago
parent
commit
cdf6e5fd2e
3 changed files with 39 additions and 205 deletions
  1. 1 1
      models/__init__.py
  2. 38 11
      models/whatsapp_account.py
  3. 0 193
      models/whatsapp_patch.py

+ 1 - 1
models/__init__.py

@@ -1,7 +1,7 @@
 from . import whatsapp_account
 from . import whatsapp_account
 from . import whatsapp_message
 from . import whatsapp_message
 from . import whatsapp_composer
 from . import whatsapp_composer
-from . import whatsapp_patch
+
 from . import mail_message
 from . import mail_message
 from . import discuss_channel
 from . import discuss_channel
 from . import res_partner
 from . import res_partner

+ 38 - 11
models/whatsapp_account.py

@@ -3,6 +3,7 @@ import requests
 import json
 import json
 import mimetypes
 import mimetypes
 import base64
 import base64
+import traceback
 from markupsafe import Markup
 from markupsafe import Markup
 
 
 from odoo import fields, models, _
 from odoo import fields, models, _
@@ -21,6 +22,15 @@ class WhatsAppAccount(models.Model):
     whatsapp_web_login = fields.Char(string="Login", readonly=False, copy=False)
     whatsapp_web_login = fields.Char(string="Login", readonly=False, copy=False)
     whatsapp_web_api_key = fields.Char(string="API Key", readonly=False, copy=False)
     whatsapp_web_api_key = fields.Char(string="API Key", readonly=False, copy=False)
 
 
+    def _send_message(self, message_type, message_values, message_id):
+        """Trap to debug who is sending messages"""
+        _logger.warning(
+            "TRAP: _send_message called! Type: %s. Stack: \n%s",
+            message_type,
+            "".join(traceback.format_stack()),
+        )
+        return super()._send_message(message_type, message_values, message_id)
+
     def get_groups(self):
     def get_groups(self):
         """
         """
         Obtiene los grupos de WhatsApp Web para la cuenta desde la base de datos de la plataforma.
         Obtiene los grupos de WhatsApp Web para la cuenta desde la base de datos de la plataforma.
@@ -150,11 +160,6 @@ class WhatsAppAccount(models.Model):
         y rutearlos al chat correcto.
         y rutearlos al chat correcto.
         Refactorizado para soportar grupos vía metadata y creación Lazy.
         Refactorizado para soportar grupos vía metadata y creación Lazy.
         """
         """
-        # Log del payload recibido para debug
-        _logger.info(
-            "DEBUG - WhatsApp Webhook Value: %s",
-            json.dumps(value, indent=4, default=str),
-        )
 
 
         if "messages" not in value and value.get("whatsapp_business_api_data", {}).get(
         if "messages" not in value and value.get("whatsapp_business_api_data", {}).get(
             "messages"
             "messages"
@@ -166,6 +171,9 @@ class WhatsAppAccount(models.Model):
         # 1. Identificar Remitente (Sender)
         # 1. Identificar Remitente (Sender)
         contacts_data = value.get("contacts", [])
         contacts_data = value.get("contacts", [])
         sender_partner = self._find_or_create_partner_from_payload(contacts_data)
         sender_partner = self._find_or_create_partner_from_payload(contacts_data)
+        original_sender_partner = (
+            sender_partner  # Preserve original sender (Me) for attribution
+        )
 
 
         # Fallback Name if partner creation failed (rare)
         # Fallback Name if partner creation failed (rare)
         sender_name = sender_partner.name if sender_partner else "Unknown"
         sender_name = sender_partner.name if sender_partner else "Unknown"
@@ -229,6 +237,22 @@ class WhatsAppAccount(models.Model):
                     sender_mobile,
                     sender_mobile,
                     messages["from"],
                     messages["from"],
                 )
                 )
+
+                # Fix: Update sender_partner to be the RECIPIENT partner for correct channel naming
+                if len(sender_mobile) >= 10:
+                    sender_partner = (
+                        self.env["res.partner"]
+                        .sudo()
+                        .search(
+                            [("mobile", "like", f"%{sender_mobile[-10:]}")], limit=1
+                        )
+                    )
+                    if sender_partner:
+                        _logger.info(
+                            "Partner destinatario encontrado para self-message: %s",
+                            sender_partner.name,
+                        )
+
             # --- RECONCILIATION LOGIC ---
             # --- RECONCILIATION LOGIC ---
             # Si viene un job_id en metadata, reconciliar el ID antes de chequear duplicados.
             # Si viene un job_id en metadata, reconciliar el ID antes de chequear duplicados.
             # Esto maneja el caso donde el "Echo" del mensaje trae el ID real y confirma el envío del worker.
             # Esto maneja el caso donde el "Echo" del mensaje trae el ID real y confirma el envío del worker.
@@ -363,7 +387,15 @@ class WhatsAppAccount(models.Model):
 
 
             # Determinar autor (Author ID)
             # Determinar autor (Author ID)
             # Preferimos usar el partner identificado del payload
             # Preferimos usar el partner identificado del payload
-            author_id = sender_partner.id if sender_partner else False
+            # Si es self-message, usar original_sender_partner (Me)
+            if is_self_message:
+                author_id = (
+                    original_sender_partner.id
+                    if original_sender_partner
+                    else self.env.ref("base.partner_root").id
+                )
+            else:
+                author_id = sender_partner.id if sender_partner else False
 
 
             # If no sender partner, try channel partner if target is channel
             # If no sender partner, try channel partner if target is channel
             if (
             if (
@@ -372,10 +404,6 @@ class WhatsAppAccount(models.Model):
             ):
             ):
                 author_id = target_record.whatsapp_partner_id.id
                 author_id = target_record.whatsapp_partner_id.id
 
 
-            if is_self_message:
-                # Si es mensaje propio, usar el partner de la compañía o OdooBot
-                author_id = self.env.ref("base.partner_root").id
-
             kwargs = {
             kwargs = {
                 "message_type": "whatsapp_message",
                 "message_type": "whatsapp_message",
                 "author_id": author_id,
                 "author_id": author_id,
@@ -484,7 +512,6 @@ class WhatsAppAccount(models.Model):
                 _logger.warning("Unsupported whatsapp message type: %s", messages)
                 _logger.warning("Unsupported whatsapp message type: %s", messages)
                 continue
                 continue
 
 
-            # Fix: Only pass whatsapp_inbound_msg_uid if valid for this channel type
             # Standard channels (like groups) do not support this param and will crash
             # Standard channels (like groups) do not support this param and will crash
             if getattr(target_record, "_name", "") == "discuss.channel":
             if getattr(target_record, "_name", "") == "discuss.channel":
                 if (
                 if (

+ 0 - 193
models/whatsapp_patch.py

@@ -1,193 +0,0 @@
-import logging
-import base64
-from odoo.addons.whatsapp.tools.whatsapp_api import WhatsAppApi
-
-_logger = logging.getLogger(__name__)
-
-# Guarda una referencia al método original
-original_get_whatsapp_document = WhatsAppApi._get_whatsapp_document
-
-
-def custom_get_whatsapp_document(self, document_id):
-    _logger.info("Ejecutando versión modificada de _get_whatsapp_document")
-
-    if self.wa_account_id.whatsapp_web_url:
-        _logger.info(
-            "Ejecutando versión modificada de _get_whatsapp_document con whatsapp web"
-        )
-        try:
-            result = base64.b64decode(document_id)
-        except Exception:
-            # Si falla la decodificación (ej. es un ID y no base64), devolvemos vacío o el ID raw
-            # para evitar crash, aunque el archivo estará corrupto.
-            # TODO: Implementar fetch real a WPPConnect
-            _logger.warning(
-                "No se pudo decodificar base64 en _get_whatsapp_document, retornando vacío"
-            )
-            result = b""
-    else:
-        result = original_get_whatsapp_document(self, document_id)
-
-    # Aquí puedes modificar 'result' si es necesario antes de devolverlo
-    return result
-
-
-# Sobrescribir el método en tiempo de ejecución
-WhatsAppApi._get_whatsapp_document = custom_get_whatsapp_document
-
-# Parche para el método _post_whatsapp_reaction para evitar errores de constraint
-try:
-    from odoo.addons.whatsapp.models.mail_message import MailMessage
-
-    # Guardar referencia al método original
-    original_post_whatsapp_reaction = MailMessage._post_whatsapp_reaction
-
-    def custom_post_whatsapp_reaction(self, reaction_content, partner_id):
-        """Parche para evitar error de constraint cuando partner_id es None"""
-        self.ensure_one()
-
-        # Si no hay partner_id, no procesar la reacción
-        if not partner_id:
-            _logger.warning(
-                "Reacción de WhatsApp recibida sin partner_id para mensaje %s - ignorando",
-                self.id,
-            )
-            return
-
-        # Llamar al método original si hay partner_id
-        return original_post_whatsapp_reaction(self, reaction_content, partner_id)
-
-    # Aplicar el parche
-    MailMessage._post_whatsapp_reaction = custom_post_whatsapp_reaction
-    _logger.info("Parche aplicado exitosamente para _post_whatsapp_reaction")
-
-except ImportError as e:
-    _logger.warning("No se pudo aplicar el parche para _post_whatsapp_reaction: %s", e)
-
-# Parche para el método wa_phone_format de phone_validation para evitar AttributeError
-try:
-    from odoo.addons.whatsapp.tools import phone_validation
-
-    # Guardar referencia al método original
-    original_wa_phone_format = phone_validation.wa_phone_format
-
-    def custom_wa_phone_format(
-        record,
-        fname=False,
-        number=False,
-        country=None,
-        force_format="INTERNATIONAL",
-        raise_exception=True,
-    ):
-        """Parche para evitar AttributeError: 'bool' object has no attribute 'italian_leading_zero'"""
-
-        # Ejecutar lógica original, pero capturando errores
-        try:
-            # Reimplementar la parte final del método original que falla
-            # Primero llamamos al método original, si funciona, perfecto
-            return original_wa_phone_format(
-                record, fname, number, country, force_format, raise_exception
-            )
-        except AttributeError as e:
-            if "italian_leading_zero" in str(e):
-                _logger.warning(
-                    "Capturado AttributeError en wa_phone_format, intentando recuperación segura: %s",
-                    e,
-                )
-
-                # Intentar replicar la lógica segura aquí si es necesario
-                # Por ahora, simplemente devolvemos el número formateado si es posible obtenerlo
-                # o relanzamos si no podemos manejarlo
-
-                # Obtener el número base
-                if not number and record and fname:
-                    record.ensure_one()
-                    number = record[fname]
-
-                if not number:
-                    return False
-
-                # Si llegamos aquí es porque falló el acceso a atributos de parsed
-                # Devolvemos el número original o intentamos un formateo básico
-                return number
-            raise e
-
-    # Una mejor aproximación: Monkey Patch directo a la función interna si es posible,
-    # o redefinir completamente la función si el error está dentro de ella y no podemos envolverla fácilmente.
-    # Dado que el error ocurre DENTRO de la función original al acceder a parsed.italian_leading_zero,
-    # necesitamos redefinir la función completa para corregir el acceso al atributo.
-
-    def safe_wa_phone_format(
-        record,
-        fname=False,
-        number=False,
-        country=None,
-        force_format="INTERNATIONAL",
-        raise_exception=True,
-    ):
-        """Versión segura de wa_phone_format que maneja correctamente los atributos de parsed"""
-
-        # Importar dependencias necesarias
-        from odoo.addons.phone_validation.tools import phone_validation as pv_tools
-
-        if not number and record and fname:
-            record.ensure_one()
-            number = record[fname]
-        if not number:
-            return False
-
-        if not country and record:
-            country = record._phone_get_country().get(record.id)
-        if not country:
-            country = record.env.company.country_id
-
-        try:
-            formatted = pv_tools.phone_format(
-                number,
-                country.code,
-                country.phone_code,
-                force_format=force_format if force_format != "WHATSAPP" else "E164",
-                raise_exception=True,
-            )
-        except Exception:
-            if raise_exception:
-                raise
-            formatted = False
-
-        if formatted and force_format == "WHATSAPP":
-            try:
-                parsed = pv_tools.phone_parse(formatted, country.code)
-            except Exception:
-                if raise_exception:
-                    raise
-                return False
-
-            zeros = ""
-            # USO SEGURO DE ATRIBUTOS (Corrección del bug original)
-            if getattr(parsed, "italian_leading_zero", False):
-                zeros = "0"
-                if getattr(parsed, "number_of_leading_zeros", False):
-                    zeros = "0" * parsed.number_of_leading_zeros
-
-            # Verificación adicional para country_code y national_number
-            country_code = getattr(parsed, "country_code", "")
-            national_number = getattr(parsed, "national_number", "")
-
-            if not country_code or not national_number:
-                # Si no se pueden obtener estos datos, intentar usar el formatted original o un fallback
-                if formatted:
-                    return formatted
-                return number
-
-            return f"{country_code}" + zeros + f"{national_number}"
-
-        return formatted
-
-    # Aplicar el parche reemplazando la función en el módulo
-    phone_validation.wa_phone_format = safe_wa_phone_format
-    _logger.info("Parche aplicado exitosamente para phone_validation.wa_phone_format")
-
-except ImportError as e:
-    _logger.warning("No se pudo aplicar el parche para phone_validation: %s", e)
-except Exception as e:
-    _logger.warning("Error al aplicar parche para phone_validation: %s", e)