| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- from odoo import models, fields, api
- from odoo.tools import groupby
- from odoo.exceptions import ValidationError
- import logging
- import markupsafe
- import requests
- import json
- import time
- import random
- import re
- import html
- import base64
- _logger = logging.getLogger(__name__)
- class WhatsAppMessage(models.Model):
- _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")
-
- @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')):
-
- 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):
- url = ''
- session_name = ''
- api_key = ''
- if self.wa_account_id and 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)
- 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)
- return
-
- for whatsapp_message in self:
- # Determinar destinatario final usando solo la nueva lógica
- final_destination = whatsapp_message._get_final_destination()
-
- if final_destination:
- group = final_destination
-
- attachment = False
- 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)
- # if retrying message then we need to unlink previous attachment
- # 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:
- whatsapp_message.mail_message_id.attachment_ids.unlink()
- 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
- elif whatsapp_message.mail_message_id.attachment_ids:
- attachment = whatsapp_message.mail_message_id.attachment_ids[0]
- #codigo para limpiar body y numero
- body = whatsapp_message.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
- 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
- text = re.sub(r'<[^>]+>', '', text)
-
- # Limpiamos múltiples saltos de línea
- text = re.sub(r'\n\s*\n\s*\n', '\n\n', text)
-
- # Limpiamos espacios en blanco al inicio y final
- body = text.strip()
-
- # Asegurar que no esté vacío
- if not body:
- body = "Mensaje de WhatsApp"
- else:
- body = "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 or not isinstance(body, str):
- _logger.error("Cuerpo del mensaje inválido: %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:
- # 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 = {
- "phone": number,
- "base64": base64_with_prefix,
- "filename": attachment.name or "file",
- "caption": body,
- "isGroup": is_group
- }
-
- 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
-
- # 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
- 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
- })
- 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
- })
- self._cr.commit()
- else:
- _logger.warning("Respuesta exitosa pero sin jobId ni id: %s", response_json)
- whatsapp_message.write({
- 'state': 'outgoing'
- })
- self._cr.commit()
- except ValueError:
- _logger.error("La respuesta no es JSON válido: %s", response.text)
- whatsapp_message.write({
- 'state': 'error'
- })
- 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'
- })
- 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'
- })
- self._cr.commit()
- time.sleep(random.randint(3, 7))
|