|
@@ -5,164 +5,248 @@ import logging
|
|
|
import markupsafe
|
|
import markupsafe
|
|
|
import requests
|
|
import requests
|
|
|
import json
|
|
import json
|
|
|
-import time
|
|
|
|
|
-import random
|
|
|
|
|
|
|
+
|
|
|
import re
|
|
import re
|
|
|
import html
|
|
import html
|
|
|
import base64
|
|
import base64
|
|
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
+
|
|
|
class WhatsAppMessage(models.Model):
|
|
class WhatsAppMessage(models.Model):
|
|
|
- _inherit = 'whatsapp.message'
|
|
|
|
|
|
|
+ _inherit = "whatsapp.message"
|
|
|
|
|
|
|
|
# Campos para soporte básico de grupos (solo por ID string, sin Many2one)
|
|
# Campos para soporte básico de grupos (solo por ID string, sin Many2one)
|
|
|
# La funcionalidad completa de grupos con Many2one está en whatsapp_web_groups
|
|
# La funcionalidad completa de grupos con Many2one está en whatsapp_web_groups
|
|
|
- recipient_type = fields.Selection([
|
|
|
|
|
- ('phone', 'Phone Number'),
|
|
|
|
|
- ('group', 'WhatsApp Group')
|
|
|
|
|
- ], string='Recipient Type', default='phone', help="Type of recipient: phone number or WhatsApp group")
|
|
|
|
|
-
|
|
|
|
|
- @api.depends('recipient_type', 'mobile_number')
|
|
|
|
|
|
|
+ recipient_type = fields.Selection(
|
|
|
|
|
+ [("phone", "Phone Number"), ("group", "WhatsApp Group")],
|
|
|
|
|
+ string="Recipient Type",
|
|
|
|
|
+ default="phone",
|
|
|
|
|
+ help="Type of recipient: phone number or WhatsApp group",
|
|
|
|
|
+ )
|
|
|
|
|
+ job_id = fields.Char(string="Job ID", index=True, copy=False)
|
|
|
|
|
+ is_batch = fields.Boolean(
|
|
|
|
|
+ string="Is Batch Message",
|
|
|
|
|
+ default=False,
|
|
|
|
|
+ help="Indicates if the message was sent as part of a batch/mass action.",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ @api.model_create_multi
|
|
|
|
|
+ def create(self, vals_list):
|
|
|
|
|
+ """Override create to handle messages coming from standard Discuss channel"""
|
|
|
|
|
+ for vals in vals_list:
|
|
|
|
|
+ mobile_number = vals.get("mobile_number")
|
|
|
|
|
+ if mobile_number and "@g.us" in mobile_number:
|
|
|
|
|
+ # 1. Clean up "discuss" formatting (e.g. +123456@g.us -> 123456@g.us)
|
|
|
|
|
+ if mobile_number.startswith("+"):
|
|
|
|
|
+ vals["mobile_number"] = mobile_number.lstrip("+")
|
|
|
|
|
+
|
|
|
|
|
+ # 2. Force recipient_type to group logic
|
|
|
|
|
+ vals["recipient_type"] = "group"
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(
|
|
|
|
|
+ "WhatsAppMessage: Auto-detected group message to %s",
|
|
|
|
|
+ vals["mobile_number"],
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return super().create(vals_list)
|
|
|
|
|
+
|
|
|
|
|
+ @api.depends("recipient_type", "mobile_number")
|
|
|
def _compute_final_recipient(self):
|
|
def _compute_final_recipient(self):
|
|
|
"""Compute the final recipient based on type"""
|
|
"""Compute the final recipient based on type"""
|
|
|
for record in self:
|
|
for record in self:
|
|
|
# Si es grupo y mobile_number termina en @g.us, usarlo directamente
|
|
# Si es grupo y mobile_number termina en @g.us, usarlo directamente
|
|
|
- if record.recipient_type == 'group' and record.mobile_number and record.mobile_number.endswith('@g.us'):
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ record.recipient_type == "group"
|
|
|
|
|
+ and record.mobile_number
|
|
|
|
|
+ and record.mobile_number.endswith("@g.us")
|
|
|
|
|
+ ):
|
|
|
record.final_recipient = record.mobile_number
|
|
record.final_recipient = record.mobile_number
|
|
|
else:
|
|
else:
|
|
|
record.final_recipient = record.mobile_number
|
|
record.final_recipient = record.mobile_number
|
|
|
-
|
|
|
|
|
- final_recipient = fields.Char('Final Recipient', compute='_compute_final_recipient',
|
|
|
|
|
- help="Final recipient (phone or group ID)")
|
|
|
|
|
-
|
|
|
|
|
- @api.depends('mobile_number', 'recipient_type')
|
|
|
|
|
|
|
+
|
|
|
|
|
+ final_recipient = fields.Char(
|
|
|
|
|
+ "Final Recipient",
|
|
|
|
|
+ compute="_compute_final_recipient",
|
|
|
|
|
+ help="Final recipient (phone or group ID)",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ @api.depends("mobile_number", "recipient_type")
|
|
|
def _compute_mobile_number_formatted(self):
|
|
def _compute_mobile_number_formatted(self):
|
|
|
"""Override SOLO para casos específicos de grupos con WhatsApp Web"""
|
|
"""Override SOLO para casos específicos de grupos con WhatsApp Web"""
|
|
|
for message in self:
|
|
for message in self:
|
|
|
# SOLO intervenir si es grupo CON WhatsApp Web configurado
|
|
# SOLO intervenir si es grupo CON WhatsApp Web configurado
|
|
|
- if (hasattr(message, 'recipient_type') and message.recipient_type == 'group' and
|
|
|
|
|
- message.wa_account_id and message.wa_account_id.whatsapp_web_url and
|
|
|
|
|
- message.mobile_number and message.mobile_number.endswith('@g.us')):
|
|
|
|
|
-
|
|
|
|
|
- message.mobile_number_formatted = message.mobile_number
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ hasattr(message, "recipient_type")
|
|
|
|
|
+ and message.recipient_type == "group"
|
|
|
|
|
+ and message.wa_account_id
|
|
|
|
|
+ and message.wa_account_id.whatsapp_web_url
|
|
|
|
|
+ and message.mobile_number
|
|
|
|
|
+ and message.mobile_number.endswith("@g.us")
|
|
|
|
|
+ ):
|
|
|
|
|
+
|
|
|
|
|
+ # CRITICAL Fix for Blacklist Crash:
|
|
|
|
|
+ # Odoo Enterprise tries to check/blacklist inbound numbers.
|
|
|
|
|
+ # Group IDs (@g.us) fail phone validation, resulting in None, which causes SQL Null Constraint error.
|
|
|
|
|
+ # By setting formatted number to empty string for INBOUND groups, we skip that potentially crashing logic.
|
|
|
|
|
+ if message.message_type == "inbound":
|
|
|
|
|
+ message.mobile_number_formatted = ""
|
|
|
|
|
+ else:
|
|
|
|
|
+ message.mobile_number_formatted = message.mobile_number
|
|
|
else:
|
|
else:
|
|
|
# TODOS LOS DEMÁS CASOS: usar lógica original sin modificar
|
|
# TODOS LOS DEMÁS CASOS: usar lógica original sin modificar
|
|
|
super(WhatsAppMessage, message)._compute_mobile_number_formatted()
|
|
super(WhatsAppMessage, message)._compute_mobile_number_formatted()
|
|
|
-
|
|
|
|
|
- @api.constrains('recipient_type', 'mobile_number')
|
|
|
|
|
|
|
+
|
|
|
|
|
+ @api.constrains("recipient_type", "mobile_number")
|
|
|
def _check_recipient_configuration(self):
|
|
def _check_recipient_configuration(self):
|
|
|
"""Validar configuración de destinatario"""
|
|
"""Validar configuración de destinatario"""
|
|
|
for record in self:
|
|
for record in self:
|
|
|
- if record.recipient_type == 'group':
|
|
|
|
|
- if not (record.mobile_number and record.mobile_number.endswith('@g.us')):
|
|
|
|
|
- raise ValidationError("Para mensajes a grupos, debe proporcionar un ID de grupo válido (@g.us)")
|
|
|
|
|
- elif record.recipient_type == 'phone':
|
|
|
|
|
- if not record.mobile_number or record.mobile_number.endswith('@g.us'):
|
|
|
|
|
- raise ValidationError("Para mensajes a teléfonos, debe proporcionar un número telefónico válido")
|
|
|
|
|
-
|
|
|
|
|
- def _whatsapp_phone_format(self, fpath=None, number=None, raise_on_format_error=False):
|
|
|
|
|
|
|
+ if record.recipient_type == "group":
|
|
|
|
|
+ if not (
|
|
|
|
|
+ record.mobile_number and record.mobile_number.endswith("@g.us")
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise ValidationError(
|
|
|
|
|
+ "Para mensajes a grupos, debe proporcionar un ID de grupo válido (@g.us)"
|
|
|
|
|
+ )
|
|
|
|
|
+ elif record.recipient_type == "phone":
|
|
|
|
|
+ if not record.mobile_number or record.mobile_number.endswith("@g.us"):
|
|
|
|
|
+ raise ValidationError(
|
|
|
|
|
+ "Para mensajes a teléfonos, debe proporcionar un número telefónico válido"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def _whatsapp_phone_format(
|
|
|
|
|
+ self, fpath=None, number=None, raise_on_format_error=False
|
|
|
|
|
+ ):
|
|
|
"""Override SOLO para casos específicos de grupos - NO interferir con funcionalidad nativa"""
|
|
"""Override SOLO para casos específicos de grupos - NO interferir con funcionalidad nativa"""
|
|
|
self.ensure_one()
|
|
self.ensure_one()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# SOLO intervenir en casos muy específicos de grupos
|
|
# SOLO intervenir en casos muy específicos de grupos
|
|
|
# Si es un mensaje a grupo Y tiene WhatsApp Web configurado
|
|
# Si es un mensaje a grupo Y tiene WhatsApp Web configurado
|
|
|
- if (hasattr(self, 'recipient_type') and self.recipient_type == 'group' and
|
|
|
|
|
- self.wa_account_id and self.wa_account_id.whatsapp_web_url):
|
|
|
|
|
-
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ hasattr(self, "recipient_type")
|
|
|
|
|
+ and self.recipient_type == "group"
|
|
|
|
|
+ and self.wa_account_id
|
|
|
|
|
+ and self.wa_account_id.whatsapp_web_url
|
|
|
|
|
+ ):
|
|
|
|
|
+
|
|
|
# Si el número es un ID de grupo (termina en @g.us), retornarlo sin validar
|
|
# Si el número es un ID de grupo (termina en @g.us), retornarlo sin validar
|
|
|
- if number and number.endswith('@g.us'):
|
|
|
|
|
|
|
+ if number and number.endswith("@g.us"):
|
|
|
return number
|
|
return number
|
|
|
- elif self.mobile_number and self.mobile_number.endswith('@g.us'):
|
|
|
|
|
|
|
+ elif self.mobile_number and self.mobile_number.endswith("@g.us"):
|
|
|
return self.mobile_number
|
|
return self.mobile_number
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# TODOS LOS DEMÁS CASOS: usar validación original sin modificar
|
|
# TODOS LOS DEMÁS CASOS: usar validación original sin modificar
|
|
|
return super()._whatsapp_phone_format(fpath, number, raise_on_format_error)
|
|
return super()._whatsapp_phone_format(fpath, number, raise_on_format_error)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
def _get_final_destination(self):
|
|
def _get_final_destination(self):
|
|
|
"""Método mejorado para obtener destino final (grupo o teléfono)"""
|
|
"""Método mejorado para obtener destino final (grupo o teléfono)"""
|
|
|
self.ensure_one()
|
|
self.ensure_one()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# Si el mobile_number es un ID de grupo (termina en @g.us)
|
|
# Si el mobile_number es un ID de grupo (termina en @g.us)
|
|
|
- if self.mobile_number and self.mobile_number.endswith('@g.us'):
|
|
|
|
|
|
|
+ if self.mobile_number and self.mobile_number.endswith("@g.us"):
|
|
|
return self.mobile_number
|
|
return self.mobile_number
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return False
|
|
return False
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
def _send_message(self, with_commit=False):
|
|
def _send_message(self, with_commit=False):
|
|
|
|
|
|
|
|
- url = ''
|
|
|
|
|
- session_name = ''
|
|
|
|
|
- api_key = ''
|
|
|
|
|
|
|
+ url = ""
|
|
|
|
|
+ session_name = ""
|
|
|
|
|
+ api_key = ""
|
|
|
|
|
|
|
|
if self.wa_account_id and self.wa_account_id.whatsapp_web_url:
|
|
if self.wa_account_id and self.wa_account_id.whatsapp_web_url:
|
|
|
url = self.wa_account_id.whatsapp_web_url
|
|
url = self.wa_account_id.whatsapp_web_url
|
|
|
- session_name = self.wa_account_id.whatsapp_web_login or ''
|
|
|
|
|
- api_key = self.wa_account_id.whatsapp_web_api_key or ''
|
|
|
|
|
- _logger.info('WHATSAPP WEB SEND MESSAGE - URL: %s, Session: %s', url, session_name)
|
|
|
|
|
|
|
+ session_name = self.wa_account_id.whatsapp_web_login or ""
|
|
|
|
|
+ api_key = self.wa_account_id.whatsapp_web_api_key or ""
|
|
|
|
|
+ _logger.info(
|
|
|
|
|
+ "WHATSAPP WEB SEND MESSAGE - URL: %s, Session: %s", url, session_name
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ group = ""
|
|
|
|
|
|
|
|
- group = ''
|
|
|
|
|
-
|
|
|
|
|
if not url or not session_name or not api_key:
|
|
if not url or not session_name or not api_key:
|
|
|
# Si no hay configuración de WhatsApp Web, usar método original
|
|
# Si no hay configuración de WhatsApp Web, usar método original
|
|
|
super()._send_message(with_commit)
|
|
super()._send_message(with_commit)
|
|
|
return
|
|
return
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
for whatsapp_message in self:
|
|
for whatsapp_message in self:
|
|
|
# Determinar destinatario final usando solo la nueva lógica
|
|
# Determinar destinatario final usando solo la nueva lógica
|
|
|
final_destination = whatsapp_message._get_final_destination()
|
|
final_destination = whatsapp_message._get_final_destination()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if final_destination:
|
|
if final_destination:
|
|
|
group = final_destination
|
|
group = final_destination
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
attachment = False
|
|
attachment = False
|
|
|
|
|
|
|
|
if whatsapp_message.wa_template_id:
|
|
if whatsapp_message.wa_template_id:
|
|
|
- record = self.env[whatsapp_message.wa_template_id.model].browse(whatsapp_message.mail_message_id.res_id)
|
|
|
|
|
- #codigo con base a whatsapp.message y whatsapp.template para generacion de adjuntos
|
|
|
|
|
- RecordModel = self.env[whatsapp_message.mail_message_id.model].with_user(whatsapp_message.create_uid)
|
|
|
|
|
- from_record = RecordModel.browse(whatsapp_message.mail_message_id.res_id)
|
|
|
|
|
|
|
+ record = self.env[whatsapp_message.wa_template_id.model].browse(
|
|
|
|
|
+ whatsapp_message.mail_message_id.res_id
|
|
|
|
|
+ )
|
|
|
|
|
+ # codigo con base a whatsapp.message y whatsapp.template para generacion de adjuntos
|
|
|
|
|
+ RecordModel = self.env[
|
|
|
|
|
+ whatsapp_message.mail_message_id.model
|
|
|
|
|
+ ].with_user(whatsapp_message.create_uid)
|
|
|
|
|
+ from_record = RecordModel.browse(
|
|
|
|
|
+ whatsapp_message.mail_message_id.res_id
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
# if retrying message then we need to unlink previous attachment
|
|
# if retrying message then we need to unlink previous attachment
|
|
|
# in case of header with report in order to generate it again
|
|
# in case of header with report in order to generate it again
|
|
|
- if whatsapp_message.wa_template_id.report_id and whatsapp_message.wa_template_id.header_type == 'document' and whatsapp_message.mail_message_id.attachment_ids:
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ whatsapp_message.wa_template_id.report_id
|
|
|
|
|
+ and whatsapp_message.wa_template_id.header_type == "document"
|
|
|
|
|
+ and whatsapp_message.mail_message_id.attachment_ids
|
|
|
|
|
+ ):
|
|
|
whatsapp_message.mail_message_id.attachment_ids.unlink()
|
|
whatsapp_message.mail_message_id.attachment_ids.unlink()
|
|
|
|
|
|
|
|
if not attachment and whatsapp_message.wa_template_id.report_id:
|
|
if not attachment and whatsapp_message.wa_template_id.report_id:
|
|
|
- attachment = whatsapp_message.wa_template_id._generate_attachment_from_report(record)
|
|
|
|
|
- if not attachment and whatsapp_message.wa_template_id.header_attachment_ids:
|
|
|
|
|
- attachment = whatsapp_message.wa_template_id.header_attachment_ids[0]
|
|
|
|
|
-
|
|
|
|
|
- if attachment and attachment not in whatsapp_message.mail_message_id.attachment_ids:
|
|
|
|
|
- whatsapp_message.mail_message_id.attachment_ids = [(4, attachment.id)]
|
|
|
|
|
- # no template
|
|
|
|
|
|
|
+ attachment = whatsapp_message.wa_template_id._generate_attachment_from_report(
|
|
|
|
|
+ record
|
|
|
|
|
+ )
|
|
|
|
|
+ if (
|
|
|
|
|
+ not attachment
|
|
|
|
|
+ and whatsapp_message.wa_template_id.header_attachment_ids
|
|
|
|
|
+ ):
|
|
|
|
|
+ attachment = whatsapp_message.wa_template_id.header_attachment_ids[
|
|
|
|
|
+ 0
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ if (
|
|
|
|
|
+ attachment
|
|
|
|
|
+ and attachment
|
|
|
|
|
+ not in whatsapp_message.mail_message_id.attachment_ids
|
|
|
|
|
+ ):
|
|
|
|
|
+ whatsapp_message.mail_message_id.attachment_ids = [
|
|
|
|
|
+ (4, attachment.id)
|
|
|
|
|
+ ]
|
|
|
|
|
+ # no template
|
|
|
elif whatsapp_message.mail_message_id.attachment_ids:
|
|
elif whatsapp_message.mail_message_id.attachment_ids:
|
|
|
attachment = whatsapp_message.mail_message_id.attachment_ids[0]
|
|
attachment = whatsapp_message.mail_message_id.attachment_ids[0]
|
|
|
|
|
|
|
|
- #codigo para limpiar body y numero
|
|
|
|
|
|
|
+ # codigo para limpiar body y numero
|
|
|
body = whatsapp_message.body
|
|
body = whatsapp_message.body
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# Asegurar que body sea string y limpiar HTML
|
|
# Asegurar que body sea string y limpiar HTML
|
|
|
if body:
|
|
if body:
|
|
|
if isinstance(body, markupsafe.Markup):
|
|
if isinstance(body, markupsafe.Markup):
|
|
|
text = html.unescape(str(body))
|
|
text = html.unescape(str(body))
|
|
|
else:
|
|
else:
|
|
|
text = str(body)
|
|
text = str(body)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# Reemplazamos las etiquetas BR y P
|
|
# Reemplazamos las etiquetas BR y P
|
|
|
- text = re.sub(r'<br\s*/?>|<BR\s*/?>', '\n', text)
|
|
|
|
|
- text = re.sub(r'<p>|<P>', '\n\n', text)
|
|
|
|
|
- text = re.sub(r'</p>|</P>', '', text)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ text = re.sub(r"<br\s*/?>|<BR\s*/?>", "\n", text)
|
|
|
|
|
+ text = re.sub(r"<p>|<P>", "\n\n", text)
|
|
|
|
|
+ text = re.sub(r"</p>|</P>", "", text)
|
|
|
|
|
+
|
|
|
# Eliminamos el resto de etiquetas HTML
|
|
# Eliminamos el resto de etiquetas HTML
|
|
|
- text = re.sub(r'<[^>]+>', '', text)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ text = re.sub(r"<[^>]+>", "", text)
|
|
|
|
|
+
|
|
|
# Limpiamos múltiples saltos de línea
|
|
# Limpiamos múltiples saltos de línea
|
|
|
- text = re.sub(r'\n\s*\n\s*\n', '\n\n', text)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ text = re.sub(r"\n\s*\n\s*\n", "\n\n", text)
|
|
|
|
|
+
|
|
|
# Limpiamos espacios en blanco al inicio y final
|
|
# Limpiamos espacios en blanco al inicio y final
|
|
|
body = text.strip()
|
|
body = text.strip()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# Asegurar que no esté vacío
|
|
# Asegurar que no esté vacío
|
|
|
if not body:
|
|
if not body:
|
|
|
body = "Mensaje de WhatsApp"
|
|
body = "Mensaje de WhatsApp"
|
|
@@ -175,162 +259,304 @@ class WhatsAppMessage(models.Model):
|
|
|
number = group
|
|
number = group
|
|
|
else:
|
|
else:
|
|
|
# Formatear número según el tipo de destinatario
|
|
# Formatear número según el tipo de destinatario
|
|
|
- if whatsapp_message.recipient_type == 'group':
|
|
|
|
|
- if whatsapp_message.mobile_number and whatsapp_message.mobile_number.endswith('@g.us'):
|
|
|
|
|
|
|
+ if whatsapp_message.recipient_type == "group":
|
|
|
|
|
+ if (
|
|
|
|
|
+ whatsapp_message.mobile_number
|
|
|
|
|
+ and whatsapp_message.mobile_number.endswith("@g.us")
|
|
|
|
|
+ ):
|
|
|
number = whatsapp_message.mobile_number
|
|
number = whatsapp_message.mobile_number
|
|
|
else:
|
|
else:
|
|
|
- _logger.error("Mensaje configurado como grupo pero sin destinatario válido")
|
|
|
|
|
|
|
+ _logger.error(
|
|
|
|
|
+ "Mensaje configurado como grupo pero sin destinatario válido"
|
|
|
|
|
+ )
|
|
|
continue
|
|
continue
|
|
|
else:
|
|
else:
|
|
|
# Lógica original para números de teléfono
|
|
# Lógica original para números de teléfono
|
|
|
number = whatsapp_message.mobile_number
|
|
number = whatsapp_message.mobile_number
|
|
|
if number:
|
|
if number:
|
|
|
- number = number.replace(' ', '').replace('+','').replace('-','')
|
|
|
|
|
|
|
+ number = (
|
|
|
|
|
+ number.replace(" ", "").replace("+", "").replace("-", "")
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
if number.startswith("52") and len(number) == 12:
|
|
if number.startswith("52") and len(number) == 12:
|
|
|
number = "521" + number[2:]
|
|
number = "521" + number[2:]
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if len(number) == 10:
|
|
if len(number) == 10:
|
|
|
number = "521" + number
|
|
number = "521" + number
|
|
|
-
|
|
|
|
|
- number = number + '@c.us'
|
|
|
|
|
|
|
+
|
|
|
|
|
+ number = number + "@c.us"
|
|
|
|
|
|
|
|
# ENVIO DE MENSAJE - Nueva API Gateway
|
|
# ENVIO DE MENSAJE - Nueva API Gateway
|
|
|
- parent_message_id = ''
|
|
|
|
|
- if whatsapp_message.mail_message_id and whatsapp_message.mail_message_id.parent_id:
|
|
|
|
|
- parent_id = whatsapp_message.mail_message_id.parent_id.wa_message_ids
|
|
|
|
|
- if parent_id:
|
|
|
|
|
- parent_message_id = parent_id[0].msg_uid
|
|
|
|
|
-
|
|
|
|
|
|
|
+ parent_message_id = ""
|
|
|
|
|
+ if (
|
|
|
|
|
+ whatsapp_message.mail_message_id
|
|
|
|
|
+ and whatsapp_message.mail_message_id.parent_id
|
|
|
|
|
+ ):
|
|
|
|
|
+ parent_id = whatsapp_message.mail_message_id.parent_id.wa_message_ids
|
|
|
|
|
+ if parent_id:
|
|
|
|
|
+ parent_message_id = parent_id[0].msg_uid
|
|
|
|
|
+
|
|
|
# Validar que tenemos un destinatario válido
|
|
# Validar que tenemos un destinatario válido
|
|
|
if not number:
|
|
if not number:
|
|
|
_logger.error("No se pudo determinar el destinatario para el mensaje")
|
|
_logger.error("No se pudo determinar el destinatario para el mensaje")
|
|
|
continue
|
|
continue
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# Validar que tenemos un cuerpo de mensaje válido
|
|
# Validar que tenemos un cuerpo de mensaje válido
|
|
|
if not body or not isinstance(body, str):
|
|
if not body or not isinstance(body, str):
|
|
|
_logger.error("Cuerpo del mensaje inválido: %s", body)
|
|
_logger.error("Cuerpo del mensaje inválido: %s", body)
|
|
|
body = "Mensaje de WhatsApp"
|
|
body = "Mensaje de WhatsApp"
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# Determinar si es grupo
|
|
# Determinar si es grupo
|
|
|
- is_group = number.endswith('@g.us') if number else False
|
|
|
|
|
-
|
|
|
|
|
|
|
+ is_group = number.endswith("@g.us") if number else False
|
|
|
|
|
+
|
|
|
# Construir URL base
|
|
# Construir URL base
|
|
|
- base_url = url.rstrip('/')
|
|
|
|
|
- endpoint = 'send-message'
|
|
|
|
|
-
|
|
|
|
|
|
|
+ base_url = url.rstrip("/")
|
|
|
|
|
+ endpoint = "send-message"
|
|
|
|
|
+
|
|
|
# Headers con autenticación
|
|
# Headers con autenticación
|
|
|
- headers = {
|
|
|
|
|
- "Content-Type": "application/json",
|
|
|
|
|
- "X-API-Key": api_key
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ headers = {"Content-Type": "application/json", "X-API-Key": api_key}
|
|
|
|
|
+
|
|
|
# Preparar payload según tipo de mensaje
|
|
# Preparar payload según tipo de mensaje
|
|
|
if attachment:
|
|
if attachment:
|
|
|
# Determinar endpoint según tipo de archivo
|
|
# Determinar endpoint según tipo de archivo
|
|
|
- mimetype = attachment.mimetype or 'application/octet-stream'
|
|
|
|
|
- if mimetype.startswith('image/'):
|
|
|
|
|
- endpoint = 'send-image'
|
|
|
|
|
- elif mimetype.startswith('video/'):
|
|
|
|
|
- endpoint = 'send-video'
|
|
|
|
|
- elif mimetype.startswith('audio/'):
|
|
|
|
|
- endpoint = 'send-voice'
|
|
|
|
|
|
|
+ mimetype = attachment.mimetype or "application/octet-stream"
|
|
|
|
|
+ if mimetype.startswith("image/"):
|
|
|
|
|
+ endpoint = "send-image"
|
|
|
|
|
+ elif mimetype.startswith("video/"):
|
|
|
|
|
+ endpoint = "send-video"
|
|
|
|
|
+ elif mimetype.startswith("audio/"):
|
|
|
|
|
+ endpoint = "send-voice"
|
|
|
else:
|
|
else:
|
|
|
- endpoint = 'send-file'
|
|
|
|
|
-
|
|
|
|
|
|
|
+ endpoint = "send-file"
|
|
|
|
|
+
|
|
|
# Convertir archivo a base64 con prefijo data URI
|
|
# Convertir archivo a base64 con prefijo data URI
|
|
|
- file_base64 = base64.b64encode(attachment.raw).decode('utf-8')
|
|
|
|
|
|
|
+ file_base64 = base64.b64encode(attachment.raw).decode("utf-8")
|
|
|
base64_with_prefix = f"data:{mimetype};base64,{file_base64}"
|
|
base64_with_prefix = f"data:{mimetype};base64,{file_base64}"
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# En wppconnect-server send-image/file espera 'phone' (singular) o 'phone' (array)
|
|
# En wppconnect-server send-image/file espera 'phone' (singular) o 'phone' (array)
|
|
|
# Para consistencia con wpp.js, usamos la misma estructura
|
|
# Para consistencia con wpp.js, usamos la misma estructura
|
|
|
payload = {
|
|
payload = {
|
|
|
- "phone": number,
|
|
|
|
|
|
|
+ "phone": number,
|
|
|
"base64": base64_with_prefix,
|
|
"base64": base64_with_prefix,
|
|
|
"filename": attachment.name or "file",
|
|
"filename": attachment.name or "file",
|
|
|
"caption": body,
|
|
"caption": body,
|
|
|
- "isGroup": is_group
|
|
|
|
|
|
|
+ "isGroup": is_group,
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if parent_message_id:
|
|
if parent_message_id:
|
|
|
payload["quotedMessageId"] = parent_message_id
|
|
payload["quotedMessageId"] = parent_message_id
|
|
|
else:
|
|
else:
|
|
|
# Mensaje de texto
|
|
# Mensaje de texto
|
|
|
# Alineación con wpp.js: phone, message, isGroup
|
|
# Alineación con wpp.js: phone, message, isGroup
|
|
|
- payload = {
|
|
|
|
|
- "phone": number,
|
|
|
|
|
- "message": body,
|
|
|
|
|
- "isGroup": is_group
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ payload = {"phone": number, "message": body, "isGroup": is_group}
|
|
|
|
|
+
|
|
|
if parent_message_id:
|
|
if parent_message_id:
|
|
|
payload["quotedMessageId"] = parent_message_id
|
|
payload["quotedMessageId"] = parent_message_id
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # Priority Logic:
|
|
|
|
|
+ # If it's NOT a batch message AND NOT a marketing campaign message, give it high priority.
|
|
|
|
|
+ # Marketing messages are identifiable by having associated marketing_trace_ids.
|
|
|
|
|
+ is_marketing = bool(
|
|
|
|
|
+ hasattr(whatsapp_message, "marketing_trace_ids")
|
|
|
|
|
+ and whatsapp_message.marketing_trace_ids
|
|
|
|
|
+ )
|
|
|
|
|
+ if not whatsapp_message.is_batch and not is_marketing:
|
|
|
|
|
+ payload["priority"] = 10
|
|
|
|
|
+
|
|
|
# Construir URL completa
|
|
# Construir URL completa
|
|
|
full_url = f"{base_url}/api/v1/{session_name}/{endpoint}"
|
|
full_url = f"{base_url}/api/v1/{session_name}/{endpoint}"
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# Log del payload para debugging
|
|
# Log del payload para debugging
|
|
|
- _logger.info("Enviando mensaje a %s (%s) usando endpoint %s", number, "grupo" if is_group else "contacto", endpoint)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ _logger.info(
|
|
|
|
|
+ "Enviando mensaje a %s (%s) usando endpoint %s",
|
|
|
|
|
+ number,
|
|
|
|
|
+ "grupo" if is_group else "contacto",
|
|
|
|
|
+ endpoint,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
# Realizar petición POST
|
|
# Realizar petición POST
|
|
|
try:
|
|
try:
|
|
|
- response = requests.post(full_url, json=payload, headers=headers, timeout=60)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ response = requests.post(
|
|
|
|
|
+ full_url, json=payload, headers=headers, timeout=60
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
# Procesar respuesta
|
|
# Procesar respuesta
|
|
|
|
|
+ _logger.info("WhatsApp API Response: %s", response.text)
|
|
|
if response.status_code == 200:
|
|
if response.status_code == 200:
|
|
|
try:
|
|
try:
|
|
|
response_json = response.json()
|
|
response_json = response.json()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# La nueva API puede devolver jobId (mensaje encolado) o id (enviado directamente)
|
|
# La nueva API puede devolver jobId (mensaje encolado) o id (enviado directamente)
|
|
|
- if 'jobId' in response_json:
|
|
|
|
|
|
|
+ if "jobId" in response_json:
|
|
|
# Mensaje encolado - si la API devuelve jobId, significa que el mensaje fue aceptado
|
|
# Mensaje encolado - si la API devuelve jobId, significa que el mensaje fue aceptado
|
|
|
# y está en proceso de envío, por lo que lo marcamos como 'sent'
|
|
# y está en proceso de envío, por lo que lo marcamos como 'sent'
|
|
|
- job_id = response_json.get('jobId')
|
|
|
|
|
- _logger.info("Mensaje aceptado por la API. Job ID: %s - Marcando como enviado", job_id)
|
|
|
|
|
- whatsapp_message.write({
|
|
|
|
|
- 'state': 'sent', # Marcar como enviado ya que fue aceptado por la API
|
|
|
|
|
- 'msg_uid': job_id
|
|
|
|
|
- })
|
|
|
|
|
- self._cr.commit()
|
|
|
|
|
- elif 'id' in response_json:
|
|
|
|
|
|
|
+ job_id = response_json.get("jobId")
|
|
|
|
|
+ _logger.info(
|
|
|
|
|
+ "Mensaje aceptado por la API. Job ID: %s - Marcando como enviado",
|
|
|
|
|
+ job_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ whatsapp_message.write(
|
|
|
|
|
+ {
|
|
|
|
|
+ "state": "sent", # Marcar como enviado ya que fue aceptado por la API
|
|
|
|
|
+ "msg_uid": job_id,
|
|
|
|
|
+ "job_id": job_id,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ if with_commit:
|
|
|
|
|
+ self._cr.commit()
|
|
|
|
|
+ elif "id" in response_json:
|
|
|
# Mensaje enviado directamente
|
|
# Mensaje enviado directamente
|
|
|
- msg_id = response_json.get('id')
|
|
|
|
|
- if isinstance(msg_id, dict) and '_serialized' in msg_id:
|
|
|
|
|
- msg_uid = msg_id['_serialized']
|
|
|
|
|
|
|
+ msg_id = response_json.get("id")
|
|
|
|
|
+ if isinstance(msg_id, dict) and "_serialized" in msg_id:
|
|
|
|
|
+ msg_uid = msg_id["_serialized"]
|
|
|
elif isinstance(msg_id, str):
|
|
elif isinstance(msg_id, str):
|
|
|
msg_uid = msg_id
|
|
msg_uid = msg_id
|
|
|
else:
|
|
else:
|
|
|
msg_uid = str(msg_id)
|
|
msg_uid = str(msg_id)
|
|
|
-
|
|
|
|
|
- _logger.info("Mensaje enviado exitosamente. ID: %s", msg_uid)
|
|
|
|
|
- whatsapp_message.write({
|
|
|
|
|
- 'state': 'sent',
|
|
|
|
|
- 'msg_uid': msg_uid
|
|
|
|
|
- })
|
|
|
|
|
- self._cr.commit()
|
|
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(
|
|
|
|
|
+ "Mensaje enviado exitosamente. ID: %s", msg_uid
|
|
|
|
|
+ )
|
|
|
|
|
+ whatsapp_message.write(
|
|
|
|
|
+ {"state": "sent", "msg_uid": msg_uid}
|
|
|
|
|
+ )
|
|
|
|
|
+ if with_commit:
|
|
|
|
|
+ self._cr.commit()
|
|
|
else:
|
|
else:
|
|
|
- _logger.warning("Respuesta exitosa pero sin jobId ni id: %s", response_json)
|
|
|
|
|
- whatsapp_message.write({
|
|
|
|
|
- 'state': 'outgoing'
|
|
|
|
|
- })
|
|
|
|
|
- self._cr.commit()
|
|
|
|
|
|
|
+ _logger.warning(
|
|
|
|
|
+ "Respuesta exitosa pero sin jobId ni id: %s",
|
|
|
|
|
+ response_json,
|
|
|
|
|
+ )
|
|
|
|
|
+ whatsapp_message.write({"state": "outgoing"})
|
|
|
|
|
+ if with_commit:
|
|
|
|
|
+ self._cr.commit()
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- _logger.error("La respuesta no es JSON válido: %s", response.text)
|
|
|
|
|
- whatsapp_message.write({
|
|
|
|
|
- 'state': 'error'
|
|
|
|
|
- })
|
|
|
|
|
- self._cr.commit()
|
|
|
|
|
|
|
+ _logger.error(
|
|
|
|
|
+ "La respuesta no es JSON válido: %s", response.text
|
|
|
|
|
+ )
|
|
|
|
|
+ whatsapp_message.write({"state": "error"})
|
|
|
|
|
+ if with_commit:
|
|
|
|
|
+ self._cr.commit()
|
|
|
else:
|
|
else:
|
|
|
- _logger.error("Error en la petición. Código: %s, Respuesta: %s", response.status_code, response.text)
|
|
|
|
|
- whatsapp_message.write({
|
|
|
|
|
- 'state': 'error'
|
|
|
|
|
- })
|
|
|
|
|
- self._cr.commit()
|
|
|
|
|
|
|
+ _logger.error(
|
|
|
|
|
+ "Error en la petición. Código: %s, Respuesta: %s",
|
|
|
|
|
+ response.status_code,
|
|
|
|
|
+ response.text,
|
|
|
|
|
+ )
|
|
|
|
|
+ whatsapp_message.write({"state": "error"})
|
|
|
|
|
+ if with_commit:
|
|
|
|
|
+ self._cr.commit()
|
|
|
except requests.exceptions.RequestException as e:
|
|
except requests.exceptions.RequestException as e:
|
|
|
_logger.error("Error de conexión al enviar mensaje: %s", str(e))
|
|
_logger.error("Error de conexión al enviar mensaje: %s", str(e))
|
|
|
- whatsapp_message.write({
|
|
|
|
|
- 'state': 'error'
|
|
|
|
|
- })
|
|
|
|
|
- self._cr.commit()
|
|
|
|
|
|
|
+ whatsapp_message.write({"state": "error"})
|
|
|
|
|
+ if with_commit:
|
|
|
|
|
+ self._cr.commit()
|
|
|
|
|
+
|
|
|
|
|
+ def _process_statuses(self, value):
|
|
|
|
|
+ """
|
|
|
|
|
+ Sobrescritura para manejar la reconciliación de IDs usando job_id.
|
|
|
|
|
+ Si la notificación trae un job_id en metadata, buscamos el mensaje por ese ID
|
|
|
|
|
+ y actualizamos el msg_uid al ID real de WhatsApp antes de procesar el estado.
|
|
|
|
|
+ """
|
|
|
|
|
+ # Pre-process statuses to reconcile IDs
|
|
|
|
|
+ for status in value.get("statuses", []):
|
|
|
|
|
+ real_wa_id = status.get("id")
|
|
|
|
|
+ # Buscar job_id en metadata (según requerimiento del usuario)
|
|
|
|
|
+ # Estructura esperada: metadata: { job_id: "..." }
|
|
|
|
|
+ metadata = status.get("metadata", {})
|
|
|
|
|
+ job_id = metadata.get("job_id")
|
|
|
|
|
+
|
|
|
|
|
+ if job_id and real_wa_id:
|
|
|
|
|
+ # Buscar mensaje por job_id que tenga un msg_uid incorrecto (el del worker)
|
|
|
|
|
+ # O simplemente buscar por job_id y asegurar que msg_uid sea el real
|
|
|
|
|
+ message = (
|
|
|
|
|
+ self.env["whatsapp.message"]
|
|
|
|
|
+ .sudo()
|
|
|
|
|
+ .search([("job_id", "=", job_id)], limit=1)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if message:
|
|
|
|
|
+ if message.msg_uid != real_wa_id:
|
|
|
|
|
+ _logger.info(
|
|
|
|
|
+ "Reconciliando WhatsApp ID: JobID %s -> Real ID %s",
|
|
|
|
|
+ job_id,
|
|
|
|
|
+ real_wa_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ # Actualizamos msg_uid al real para que el super() lo encuentre
|
|
|
|
|
+ message.msg_uid = real_wa_id
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.info("Mensaje ya reconciliado para JobID %s", job_id)
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning(
|
|
|
|
|
+ "Recibido status con JobID %s pero no se encontró mensaje en Odoo",
|
|
|
|
|
+ job_id,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Call original implementation to handle standard status updates (sent, delivered, read, etc.)
|
|
|
|
|
+ return super()._process_statuses(value)
|
|
|
|
|
+
|
|
|
|
|
+ def _update_message_fetched_seen(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Sobrescritura para manejar concurrencia en la actualización de discuss.channel.member.
|
|
|
|
|
+ Usa bloqueo de fila (SELECT FOR UPDATE) para evitar SERIALIZATION_FAILURE.
|
|
|
|
|
+ """
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+ if self.mail_message_id.model != "discuss.channel":
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ channel = self.env["discuss.channel"].browse(self.mail_message_id.res_id)
|
|
|
|
|
+
|
|
|
|
|
+ # Buscar el miembro usando SQL directo para bloquear la fila antes de leer/escribir
|
|
|
|
|
+ # Odoo ORM no soporta lock explícito fácil en search(), así que primero buscamos ID y luego bloqueamos.
|
|
|
|
|
+ channel_member = channel.channel_member_ids.filtered(
|
|
|
|
|
+ lambda cm: cm.partner_id == channel.whatsapp_partner_id
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if not channel_member:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ channel_member = channel_member[0]
|
|
|
|
|
+
|
|
|
|
|
+ # --- CONCURRENCY FIX START ---
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Bloquear la fila específica del miembro del canal
|
|
|
|
|
+ self.env.cr.execute(
|
|
|
|
|
+ "SELECT id FROM discuss_channel_member WHERE id = %s FOR UPDATE",
|
|
|
|
|
+ (channel_member.id,),
|
|
|
|
|
+ )
|
|
|
|
|
+ # Invalidar caché para asegurar que leemos datos frescos después del bloqueo
|
|
|
|
|
+ channel_member.invalidate_recordset()
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.warning(
|
|
|
|
|
+ "No se pudo bloquear discuss_channel_member %s: %s",
|
|
|
|
|
+ channel_member.id,
|
|
|
|
|
+ e,
|
|
|
|
|
+ )
|
|
|
|
|
+ # --- CONCURRENCY FIX END ---
|
|
|
|
|
+
|
|
|
|
|
+ notification_type = None
|
|
|
|
|
+ if self.state == "read":
|
|
|
|
|
+ channel_member.write(
|
|
|
|
|
+ {
|
|
|
|
|
+ "fetched_message_id": max(
|
|
|
|
|
+ channel_member.fetched_message_id.id, self.mail_message_id.id
|
|
|
|
|
+ ),
|
|
|
|
|
+ "seen_message_id": self.mail_message_id.id,
|
|
|
|
|
+ "last_seen_dt": fields.Datetime.now(),
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ notification_type = "discuss.channel.member/seen"
|
|
|
|
|
+ elif self.state == "delivered":
|
|
|
|
|
+ channel_member.write({"fetched_message_id": self.mail_message_id.id})
|
|
|
|
|
+ notification_type = "discuss.channel.member/fetched"
|
|
|
|
|
|
|
|
- time.sleep(random.randint(3, 7))
|
|
|
|
|
|
|
+ if notification_type:
|
|
|
|
|
+ channel._bus_send(
|
|
|
|
|
+ notification_type,
|
|
|
|
|
+ {
|
|
|
|
|
+ "channel_id": channel.id,
|
|
|
|
|
+ "id": channel_member.id,
|
|
|
|
|
+ "last_message_id": self.mail_message_id.id,
|
|
|
|
|
+ "partner_id": channel.whatsapp_partner_id.id,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|