|
@@ -1,146 +1,576 @@
|
|
|
from odoo import models, fields, api
|
|
from odoo import models, fields, api
|
|
|
from odoo.tools import groupby
|
|
from odoo.tools import groupby
|
|
|
|
|
+from odoo.exceptions import ValidationError
|
|
|
import logging
|
|
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)
|
|
|
|
|
+ # 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",
|
|
|
|
|
+ )
|
|
|
|
|
+ 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):
|
|
|
|
|
+ """Compute the final recipient based on type"""
|
|
|
|
|
+ for record in self:
|
|
|
|
|
+ # 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")
|
|
|
|
|
+ ):
|
|
|
|
|
+ record.final_recipient = record.mobile_number
|
|
|
|
|
+ else:
|
|
|
|
|
+ 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")
|
|
|
|
|
+ def _compute_mobile_number_formatted(self):
|
|
|
|
|
+ """Override SOLO para casos específicos de grupos con WhatsApp Web"""
|
|
|
|
|
+ for message in self:
|
|
|
|
|
+ # 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")
|
|
|
|
|
+ ):
|
|
|
|
|
+
|
|
|
|
|
+ # 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:
|
|
|
|
|
+ # TODOS LOS DEMÁS CASOS: usar lógica original sin modificar
|
|
|
|
|
+ super(WhatsAppMessage, message)._compute_mobile_number_formatted()
|
|
|
|
|
+
|
|
|
|
|
+ @api.constrains("recipient_type", "mobile_number")
|
|
|
|
|
+ def _check_recipient_configuration(self):
|
|
|
|
|
+ """Validar configuración de destinatario"""
|
|
|
|
|
+ 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
|
|
|
|
|
+ ):
|
|
|
|
|
+ """Override SOLO para casos específicos de grupos - NO interferir con funcionalidad nativa"""
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+
|
|
|
|
|
+ # SOLO intervenir en casos muy específicos de grupos
|
|
|
|
|
+ # 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
|
|
|
|
|
+ ):
|
|
|
|
|
+
|
|
|
|
|
+ # Si el número es un ID de grupo (termina en @g.us), retornarlo sin validar
|
|
|
|
|
+ if number and number.endswith("@g.us"):
|
|
|
|
|
+ return number
|
|
|
|
|
+ elif self.mobile_number and self.mobile_number.endswith("@g.us"):
|
|
|
|
|
+ return self.mobile_number
|
|
|
|
|
+
|
|
|
|
|
+ # TODOS LOS DEMÁS CASOS: usar validación original sin modificar
|
|
|
|
|
+ return super()._whatsapp_phone_format(fpath, number, raise_on_format_error)
|
|
|
|
|
+
|
|
|
|
|
+ def _get_final_destination(self):
|
|
|
|
|
+ """Método mejorado para obtener destino final (grupo o teléfono)"""
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+
|
|
|
|
|
+ # Si el mobile_number es un ID de grupo (termina en @g.us)
|
|
|
|
|
+ if self.mobile_number and self.mobile_number.endswith("@g.us"):
|
|
|
|
|
+ return self.mobile_number
|
|
|
|
|
+
|
|
|
|
|
+ return False
|
|
|
|
|
|
|
|
-
|
|
|
|
|
def _send_message(self, with_commit=False):
|
|
def _send_message(self, with_commit=False):
|
|
|
|
|
|
|
|
- url = ''
|
|
|
|
|
|
|
+ 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
|
|
|
- _logger.info('WHATSAPP WEB SEND MESSAGE' + 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
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- group = ''
|
|
|
|
|
-
|
|
|
|
|
- if not url:
|
|
|
|
|
|
|
+ group = ""
|
|
|
|
|
+
|
|
|
|
|
+ if not url or not session_name or not api_key:
|
|
|
|
|
+ # Si no hay configuración de WhatsApp Web, usar método original
|
|
|
super()._send_message(with_commit)
|
|
super()._send_message(with_commit)
|
|
|
-
|
|
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
for whatsapp_message in self:
|
|
for whatsapp_message in self:
|
|
|
- #verificacion envio a grupo
|
|
|
|
|
- #plantilla dada de alta en x_plantillas_whatsapp
|
|
|
|
|
- if 'marketing.trace' in self.env:
|
|
|
|
|
- marketing_traces = self.env['marketing.trace'].sudo().search([('whatsapp_message_id', '=', whatsapp_message.id)])
|
|
|
|
|
- for marketing_trace in marketing_traces:
|
|
|
|
|
- if 'x_studio_grupo_whatsapp' in marketing_trace.activity_id and marketing_trace.activity_id.x_studio_grupo_whatsapp:
|
|
|
|
|
- group = marketing_trace.activity_id.x_studio_grupo_whatsapp.x_studio_destinatario
|
|
|
|
|
-
|
|
|
|
|
- if 'x_notificaciones_whats' in self.env and not group:
|
|
|
|
|
- notificaciones = self.env['x_notificaciones_whats'].sudo().search([('x_studio_plantilla_de_whatsapp', '=', whatsapp_message.wa_template_id.id)])
|
|
|
|
|
- if notificaciones:
|
|
|
|
|
- _logger.info('template encontrado')
|
|
|
|
|
- if not group:
|
|
|
|
|
- for notificacion in notificaciones:
|
|
|
|
|
- if not notificacion.x_studio_partner_unico:
|
|
|
|
|
- group = notificacion.x_studio_destinatario
|
|
|
|
|
- break
|
|
|
|
|
-
|
|
|
|
|
|
|
+ # Determinar destinatario final usando solo la nueva lógica
|
|
|
|
|
+ final_destination = whatsapp_message._get_final_destination()
|
|
|
|
|
+
|
|
|
|
|
+ if 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
|
|
|
- if isinstance(body, markupsafe.Markup):
|
|
|
|
|
- text = html.unescape(str(body))
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # Asegurar que body sea string y limpiar HTML
|
|
|
|
|
+ if body:
|
|
|
|
|
+ if isinstance(body, markupsafe.Markup):
|
|
|
|
|
+ text = html.unescape(str(body))
|
|
|
|
|
+ else:
|
|
|
|
|
+ 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()
|
|
|
|
|
|
|
|
- number = whatsapp_message.mobile_number
|
|
|
|
|
- number = number.replace(' ', '').replace('+','')
|
|
|
|
|
-
|
|
|
|
|
- if number.startswith("52") and len(number) == 12:
|
|
|
|
|
- number = "521" + number[2:]
|
|
|
|
|
-
|
|
|
|
|
- # ENVIO DE MENSAJE
|
|
|
|
|
- # Headers de la petición, si es necesario
|
|
|
|
|
- headers = {
|
|
|
|
|
- "Content-Type": "application/json"
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- number = group if group else number + '@c.us'
|
|
|
|
|
-
|
|
|
|
|
- #$wa::sendMessage("521{$fields_data[$settings['borax_whatsapp_mobile']]}@c.us", ['type' => 'MessageMedia', 'args' => [mime_content_type($file), base64_encode(file_get_contents($file)), $filename, $filesize]], ['caption' => $borax_whatsapp_mensaje]);
|
|
|
|
|
- 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
|
|
|
|
|
|
|
+ # Asegurar que no esté vacío (solo si no hay adjunto)
|
|
|
|
|
+ if not body:
|
|
|
|
|
+ body = "" if attachment else "Mensaje de WhatsApp"
|
|
|
|
|
+ else:
|
|
|
|
|
+ body = "" if attachment else "Mensaje de WhatsApp"
|
|
|
|
|
+
|
|
|
|
|
+ # Determinar número/destinatario final
|
|
|
|
|
+ if group:
|
|
|
|
|
+ # Si ya hay un grupo determinado, usarlo
|
|
|
|
|
+ number = group
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 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")
|
|
|
|
|
+ ):
|
|
|
|
|
+ number = whatsapp_message.mobile_number
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.error(
|
|
|
|
|
+ "Mensaje configurado como grupo pero sin destinatario válido"
|
|
|
|
|
+ )
|
|
|
|
|
+ continue
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Lógica original para números de teléfono
|
|
|
|
|
+ number = whatsapp_message.mobile_number
|
|
|
|
|
+ if number:
|
|
|
|
|
+ number = (
|
|
|
|
|
+ number.replace(" ", "").replace("+", "").replace("-", "")
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if number.startswith("52") and len(number) == 12:
|
|
|
|
|
+ number = "521" + number[2:]
|
|
|
|
|
+
|
|
|
|
|
+ if len(number) == 10:
|
|
|
|
|
+ number = "521" + number
|
|
|
|
|
+
|
|
|
|
|
+ number = number + "@c.us"
|
|
|
|
|
+
|
|
|
|
|
+ # 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
|
|
|
|
|
+
|
|
|
|
|
+ # Validar que tenemos un destinatario válido
|
|
|
|
|
+ if not number:
|
|
|
|
|
+ _logger.error("No se pudo determinar el destinatario para el mensaje")
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # Validar que tenemos un cuerpo de mensaje válido
|
|
|
|
|
+ if not body and not attachment:
|
|
|
|
|
+ _logger.error("Cuerpo del mensaje inválido: %s", body)
|
|
|
|
|
+ body = "Mensaje de WhatsApp"
|
|
|
|
|
+ elif body and not isinstance(body, str):
|
|
|
|
|
+ _logger.error("Cuerpo del mensaje inválido (tipo incorrecto): %s", body)
|
|
|
|
|
+ body = "Mensaje de WhatsApp"
|
|
|
|
|
+
|
|
|
|
|
+ # Determinar si es grupo
|
|
|
|
|
+ is_group = number.endswith("@g.us") if number else False
|
|
|
|
|
+
|
|
|
|
|
+ # Construir URL base
|
|
|
|
|
+ base_url = url.rstrip("/")
|
|
|
|
|
+ endpoint = "send-message"
|
|
|
|
|
+
|
|
|
|
|
+ # Headers con autenticación
|
|
|
|
|
+ headers = {"Content-Type": "application/json", "X-API-Key": api_key}
|
|
|
|
|
+
|
|
|
|
|
+ # Preparar payload según tipo de mensaje
|
|
|
if attachment:
|
|
if attachment:
|
|
|
|
|
+ # 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"
|
|
|
|
|
+ else:
|
|
|
|
|
+ endpoint = "send-file"
|
|
|
|
|
+
|
|
|
|
|
+ # Convertir archivo a base64 con prefijo data URI
|
|
|
|
|
+ file_base64 = base64.b64encode(attachment.raw).decode("utf-8")
|
|
|
|
|
+ base64_with_prefix = f"data:{mimetype};base64,{file_base64}"
|
|
|
|
|
+
|
|
|
|
|
+ # En wppconnect-server send-image/file espera 'phone' (singular) o 'phone' (array)
|
|
|
|
|
+ # Para consistencia con wpp.js, usamos la misma estructura
|
|
|
payload = {
|
|
payload = {
|
|
|
- "method": "sendMessage",
|
|
|
|
|
- "args": [number, {'type': 'MessageMedia', 'args': [attachment.mimetype, base64.b64encode(attachment.raw).decode('utf-8'), attachment.name, attachment.file_size]}, {'caption': body}]
|
|
|
|
|
- }
|
|
|
|
|
- else:
|
|
|
|
|
- payload = {
|
|
|
|
|
- "method": "sendMessage",
|
|
|
|
|
- "args": [number, body, {}]
|
|
|
|
|
|
|
+ "phone": number,
|
|
|
|
|
+ "base64": base64_with_prefix,
|
|
|
|
|
+ "filename": attachment.name or "file",
|
|
|
|
|
+ "caption": body,
|
|
|
|
|
+ "isGroup": is_group,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if parent_message_id:
|
|
|
|
|
- payload['args'][2]['quotedMessageId'] = parent_message_id
|
|
|
|
|
-
|
|
|
|
|
- # Realizando la petición POST
|
|
|
|
|
- response = requests.post(url, data=json.dumps(payload), headers=headers)
|
|
|
|
|
-
|
|
|
|
|
- # Verificando si la respuesta contiene data->id
|
|
|
|
|
- if response.status_code == 200:
|
|
|
|
|
- response_json = response.json()
|
|
|
|
|
- if "_data" in response_json and "id" in response_json["_data"]:
|
|
|
|
|
- _logger.info(f"Petición exitosa. ID: {response_json['_data']['id']['id']}")
|
|
|
|
|
- whatsapp_message.write({
|
|
|
|
|
- 'state': 'sent',
|
|
|
|
|
- 'msg_uid': response_json['_data']['id']['_serialized']
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ if parent_message_id:
|
|
|
|
|
+ payload["quotedMessageId"] = parent_message_id
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Mensaje de texto
|
|
|
|
|
+ # Alineación con wpp.js: phone, message, isGroup
|
|
|
|
|
+ payload = {"phone": number, "message": body, "isGroup": is_group}
|
|
|
|
|
+
|
|
|
|
|
+ if 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
|
|
|
|
|
+ full_url = f"{base_url}/api/v1/{session_name}/{endpoint}"
|
|
|
|
|
+
|
|
|
|
|
+ # Log del payload para debugging
|
|
|
|
|
+ _logger.info(
|
|
|
|
|
+ "Enviando mensaje a %s (%s) usando endpoint %s",
|
|
|
|
|
+ number,
|
|
|
|
|
+ "grupo" if is_group else "contacto",
|
|
|
|
|
+ endpoint,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Realizar petición POST
|
|
|
|
|
+ try:
|
|
|
|
|
+ response = requests.post(
|
|
|
|
|
+ full_url, json=payload, headers=headers, timeout=60
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Procesar respuesta
|
|
|
|
|
+ _logger.info("WhatsApp API Response: %s", response.text)
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ try:
|
|
|
|
|
+ response_json = response.json()
|
|
|
|
|
+
|
|
|
|
|
+ # La nueva API puede devolver jobId (mensaje encolado) o id (enviado directamente)
|
|
|
|
|
+ if "jobId" in response_json:
|
|
|
|
|
+ # 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'
|
|
|
|
|
+ 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
|
|
|
|
|
+ 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):
|
|
|
|
|
+ msg_uid = msg_id
|
|
|
|
|
+ else:
|
|
|
|
|
+ msg_uid = str(msg_id)
|
|
|
|
|
+
|
|
|
|
|
+ _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:
|
|
|
|
|
+ _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:
|
|
|
|
|
+ _logger.error(
|
|
|
|
|
+ "La respuesta no es JSON válido: %s", response.text
|
|
|
|
|
+ )
|
|
|
|
|
+ whatsapp_message.write({"state": "error"})
|
|
|
|
|
+ if with_commit:
|
|
|
|
|
+ self._cr.commit()
|
|
|
|
|
+ else:
|
|
|
|
|
+ _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:
|
|
|
|
|
+ _logger.error("Error de conexión al enviar mensaje: %s", str(e))
|
|
|
|
|
+ whatsapp_message.write({"state": "error"})
|
|
|
|
|
+ if with_commit:
|
|
|
self._cr.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
|
|
|
|
|
+ metadata = value.get("metadata", {})
|
|
|
|
|
+ job_id = metadata.get("job_id")
|
|
|
|
|
+
|
|
|
|
|
+ for status in value.get("statuses", []):
|
|
|
|
|
+ real_wa_id = status.get("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:
|
|
else:
|
|
|
- _logger.info("La respuesta no contiene 'data->id'.")
|
|
|
|
|
- else:
|
|
|
|
|
- _logger.info(f"Error en la petición. Código de estado: {response.status_code}")
|
|
|
|
|
|
|
+ _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 (NO KEY UPDATE SKIP LOCKED) 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 asociado al partner de WhatsApp
|
|
|
|
|
+ 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]
|
|
|
|
|
+ member_id = channel_member.id
|
|
|
|
|
+
|
|
|
|
|
+ notification_type = None
|
|
|
|
|
+ updated_rows = 0
|
|
|
|
|
+
|
|
|
|
|
+ if self.state == "read":
|
|
|
|
|
+ # Intentar actualizar usando SKIP LOCKED
|
|
|
|
|
+ # Si el registro está bloqueado, simplemente saltamos esta actualización
|
|
|
|
|
+ # ya que fetched/seen son contadores monotónicos o de estado reciente
|
|
|
|
|
+ self.env.cr.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ UPDATE discuss_channel_member
|
|
|
|
|
+ SET fetched_message_id = GREATEST(fetched_message_id, %s),
|
|
|
|
|
+ seen_message_id = %s,
|
|
|
|
|
+ last_seen_dt = NOW()
|
|
|
|
|
+ WHERE id IN (
|
|
|
|
|
+ SELECT id FROM discuss_channel_member WHERE id = %s
|
|
|
|
|
+ FOR NO KEY UPDATE SKIP LOCKED
|
|
|
|
|
+ )
|
|
|
|
|
+ RETURNING id
|
|
|
|
|
+ """,
|
|
|
|
|
+ (self.mail_message_id.id, self.mail_message_id.id, member_id),
|
|
|
|
|
+ )
|
|
|
|
|
+ # Si fetchone devuelve algo, significa que actualizó. Si es None, saltó por bloqueo.
|
|
|
|
|
+ if self.env.cr.fetchone():
|
|
|
|
|
+ channel_member.invalidate_recordset(
|
|
|
|
|
+ ["fetched_message_id", "seen_message_id", "last_seen_dt"]
|
|
|
|
|
+ )
|
|
|
|
|
+ notification_type = "discuss.channel.member/seen"
|
|
|
|
|
+
|
|
|
|
|
+ elif self.state == "delivered":
|
|
|
|
|
+ self.env.cr.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ UPDATE discuss_channel_member
|
|
|
|
|
+ SET fetched_message_id = GREATEST(fetched_message_id, %s)
|
|
|
|
|
+ WHERE id IN (
|
|
|
|
|
+ SELECT id FROM discuss_channel_member WHERE id = %s
|
|
|
|
|
+ FOR NO KEY UPDATE SKIP LOCKED
|
|
|
|
|
+ )
|
|
|
|
|
+ RETURNING id
|
|
|
|
|
+ """,
|
|
|
|
|
+ (self.mail_message_id.id, member_id),
|
|
|
|
|
+ )
|
|
|
|
|
+ if self.env.cr.fetchone():
|
|
|
|
|
+ channel_member.invalidate_recordset(["fetched_message_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,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|