whatsapp_message.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. from odoo import models, fields, api
  2. from odoo.tools import groupby
  3. from odoo.exceptions import ValidationError
  4. import logging
  5. import markupsafe
  6. import requests
  7. import json
  8. import time
  9. import random
  10. import re
  11. import html
  12. import base64
  13. _logger = logging.getLogger(__name__)
  14. class WhatsAppMessage(models.Model):
  15. _inherit = 'whatsapp.message'
  16. # Campos para soporte básico de grupos (solo por ID string, sin Many2one)
  17. # La funcionalidad completa de grupos con Many2one está en whatsapp_web_groups
  18. recipient_type = fields.Selection([
  19. ('phone', 'Phone Number'),
  20. ('group', 'WhatsApp Group')
  21. ], string='Recipient Type', default='phone', help="Type of recipient: phone number or WhatsApp group")
  22. @api.depends('recipient_type', 'mobile_number')
  23. def _compute_final_recipient(self):
  24. """Compute the final recipient based on type"""
  25. for record in self:
  26. # Si es grupo y mobile_number termina en @g.us, usarlo directamente
  27. if record.recipient_type == 'group' and record.mobile_number and record.mobile_number.endswith('@g.us'):
  28. record.final_recipient = record.mobile_number
  29. else:
  30. record.final_recipient = record.mobile_number
  31. final_recipient = fields.Char('Final Recipient', compute='_compute_final_recipient',
  32. help="Final recipient (phone or group ID)")
  33. @api.depends('mobile_number', 'recipient_type')
  34. def _compute_mobile_number_formatted(self):
  35. """Override SOLO para casos específicos de grupos con WhatsApp Web"""
  36. for message in self:
  37. # SOLO intervenir si es grupo CON WhatsApp Web configurado
  38. if (hasattr(message, 'recipient_type') and message.recipient_type == 'group' and
  39. message.wa_account_id and message.wa_account_id.whatsapp_web_url and
  40. message.mobile_number and message.mobile_number.endswith('@g.us')):
  41. message.mobile_number_formatted = message.mobile_number
  42. else:
  43. # TODOS LOS DEMÁS CASOS: usar lógica original sin modificar
  44. super(WhatsAppMessage, message)._compute_mobile_number_formatted()
  45. @api.constrains('recipient_type', 'mobile_number')
  46. def _check_recipient_configuration(self):
  47. """Validar configuración de destinatario"""
  48. for record in self:
  49. if record.recipient_type == 'group':
  50. if not (record.mobile_number and record.mobile_number.endswith('@g.us')):
  51. raise ValidationError("Para mensajes a grupos, debe proporcionar un ID de grupo válido (@g.us)")
  52. elif record.recipient_type == 'phone':
  53. if not record.mobile_number or record.mobile_number.endswith('@g.us'):
  54. raise ValidationError("Para mensajes a teléfonos, debe proporcionar un número telefónico válido")
  55. def _whatsapp_phone_format(self, fpath=None, number=None, raise_on_format_error=False):
  56. """Override SOLO para casos específicos de grupos - NO interferir con funcionalidad nativa"""
  57. self.ensure_one()
  58. # SOLO intervenir en casos muy específicos de grupos
  59. # Si es un mensaje a grupo Y tiene WhatsApp Web configurado
  60. if (hasattr(self, 'recipient_type') and self.recipient_type == 'group' and
  61. self.wa_account_id and self.wa_account_id.whatsapp_web_url):
  62. # Si el número es un ID de grupo (termina en @g.us), retornarlo sin validar
  63. if number and number.endswith('@g.us'):
  64. return number
  65. elif self.mobile_number and self.mobile_number.endswith('@g.us'):
  66. return self.mobile_number
  67. # TODOS LOS DEMÁS CASOS: usar validación original sin modificar
  68. return super()._whatsapp_phone_format(fpath, number, raise_on_format_error)
  69. def _get_final_destination(self):
  70. """Método mejorado para obtener destino final (grupo o teléfono)"""
  71. self.ensure_one()
  72. # Si el mobile_number es un ID de grupo (termina en @g.us)
  73. if self.mobile_number and self.mobile_number.endswith('@g.us'):
  74. return self.mobile_number
  75. return False
  76. def _send_message(self, with_commit=False):
  77. url = ''
  78. session_name = ''
  79. api_key = ''
  80. if self.wa_account_id and self.wa_account_id.whatsapp_web_url:
  81. url = self.wa_account_id.whatsapp_web_url
  82. session_name = self.wa_account_id.whatsapp_web_login or ''
  83. api_key = self.wa_account_id.whatsapp_web_api_key or ''
  84. _logger.info('WHATSAPP WEB SEND MESSAGE - URL: %s, Session: %s', url, session_name)
  85. group = ''
  86. if not url or not session_name or not api_key:
  87. # Si no hay configuración de WhatsApp Web, usar método original
  88. super()._send_message(with_commit)
  89. return
  90. for whatsapp_message in self:
  91. # Determinar destinatario final usando solo la nueva lógica
  92. final_destination = whatsapp_message._get_final_destination()
  93. if final_destination:
  94. group = final_destination
  95. attachment = False
  96. if whatsapp_message.wa_template_id:
  97. record = self.env[whatsapp_message.wa_template_id.model].browse(whatsapp_message.mail_message_id.res_id)
  98. #codigo con base a whatsapp.message y whatsapp.template para generacion de adjuntos
  99. RecordModel = self.env[whatsapp_message.mail_message_id.model].with_user(whatsapp_message.create_uid)
  100. from_record = RecordModel.browse(whatsapp_message.mail_message_id.res_id)
  101. # if retrying message then we need to unlink previous attachment
  102. # in case of header with report in order to generate it again
  103. 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:
  104. whatsapp_message.mail_message_id.attachment_ids.unlink()
  105. if not attachment and whatsapp_message.wa_template_id.report_id:
  106. attachment = whatsapp_message.wa_template_id._generate_attachment_from_report(record)
  107. if not attachment and whatsapp_message.wa_template_id.header_attachment_ids:
  108. attachment = whatsapp_message.wa_template_id.header_attachment_ids[0]
  109. if attachment and attachment not in whatsapp_message.mail_message_id.attachment_ids:
  110. whatsapp_message.mail_message_id.attachment_ids = [(4, attachment.id)]
  111. # no template
  112. elif whatsapp_message.mail_message_id.attachment_ids:
  113. attachment = whatsapp_message.mail_message_id.attachment_ids[0]
  114. #codigo para limpiar body y numero
  115. body = whatsapp_message.body
  116. # Asegurar que body sea string y limpiar HTML
  117. if body:
  118. if isinstance(body, markupsafe.Markup):
  119. text = html.unescape(str(body))
  120. else:
  121. text = str(body)
  122. # Reemplazamos las etiquetas BR y P
  123. text = re.sub(r'<br\s*/?>|<BR\s*/?>', '\n', text)
  124. text = re.sub(r'<p>|<P>', '\n\n', text)
  125. text = re.sub(r'</p>|</P>', '', text)
  126. # Eliminamos el resto de etiquetas HTML
  127. text = re.sub(r'<[^>]+>', '', text)
  128. # Limpiamos múltiples saltos de línea
  129. text = re.sub(r'\n\s*\n\s*\n', '\n\n', text)
  130. # Limpiamos espacios en blanco al inicio y final
  131. body = text.strip()
  132. # Asegurar que no esté vacío
  133. if not body:
  134. body = "Mensaje de WhatsApp"
  135. else:
  136. body = "Mensaje de WhatsApp"
  137. # Determinar número/destinatario final
  138. if group:
  139. # Si ya hay un grupo determinado, usarlo
  140. number = group
  141. else:
  142. # Formatear número según el tipo de destinatario
  143. if whatsapp_message.recipient_type == 'group':
  144. if whatsapp_message.mobile_number and whatsapp_message.mobile_number.endswith('@g.us'):
  145. number = whatsapp_message.mobile_number
  146. else:
  147. _logger.error("Mensaje configurado como grupo pero sin destinatario válido")
  148. continue
  149. else:
  150. # Lógica original para números de teléfono
  151. number = whatsapp_message.mobile_number
  152. if number:
  153. number = number.replace(' ', '').replace('+','').replace('-','')
  154. if number.startswith("52") and len(number) == 12:
  155. number = "521" + number[2:]
  156. if len(number) == 10:
  157. number = "521" + number
  158. number = number + '@c.us'
  159. # ENVIO DE MENSAJE - Nueva API Gateway
  160. parent_message_id = ''
  161. if whatsapp_message.mail_message_id and whatsapp_message.mail_message_id.parent_id:
  162. parent_id = whatsapp_message.mail_message_id.parent_id.wa_message_ids
  163. if parent_id:
  164. parent_message_id = parent_id[0].msg_uid
  165. # Validar que tenemos un destinatario válido
  166. if not number:
  167. _logger.error("No se pudo determinar el destinatario para el mensaje")
  168. continue
  169. # Validar que tenemos un cuerpo de mensaje válido
  170. if not body or not isinstance(body, str):
  171. _logger.error("Cuerpo del mensaje inválido: %s", body)
  172. body = "Mensaje de WhatsApp"
  173. # Determinar si es grupo
  174. is_group = number.endswith('@g.us') if number else False
  175. # Construir URL base
  176. base_url = url.rstrip('/')
  177. endpoint = 'send-message'
  178. # Headers con autenticación
  179. headers = {
  180. "Content-Type": "application/json",
  181. "X-API-Key": api_key
  182. }
  183. # Preparar payload según tipo de mensaje
  184. if attachment:
  185. # Determinar endpoint según tipo de archivo
  186. mimetype = attachment.mimetype or 'application/octet-stream'
  187. if mimetype.startswith('image/'):
  188. endpoint = 'send-image'
  189. elif mimetype.startswith('video/'):
  190. endpoint = 'send-video'
  191. elif mimetype.startswith('audio/'):
  192. endpoint = 'send-voice'
  193. else:
  194. endpoint = 'send-file'
  195. # Convertir archivo a base64 con prefijo data URI
  196. file_base64 = base64.b64encode(attachment.raw).decode('utf-8')
  197. base64_with_prefix = f"data:{mimetype};base64,{file_base64}"
  198. payload = {
  199. "phone": [number], # Array para send-image/send-file
  200. "base64": base64_with_prefix,
  201. "filename": attachment.name or "file",
  202. "caption": body,
  203. "isGroup": is_group
  204. }
  205. if parent_message_id:
  206. payload["quotedMessageId"] = parent_message_id
  207. else:
  208. # Mensaje de texto
  209. payload = {
  210. "phone": number, # String para send-message
  211. "message": body,
  212. "isGroup": is_group
  213. }
  214. if parent_message_id:
  215. payload["quotedMessageId"] = parent_message_id
  216. # Construir URL completa
  217. full_url = f"{base_url}/api/v1/{session_name}/{endpoint}"
  218. # Log del payload para debugging
  219. _logger.info("Enviando mensaje a %s (%s) usando endpoint %s", number, "grupo" if is_group else "contacto", endpoint)
  220. # Realizar petición POST
  221. try:
  222. response = requests.post(full_url, json=payload, headers=headers, timeout=60)
  223. # Procesar respuesta
  224. if response.status_code == 200:
  225. try:
  226. response_json = response.json()
  227. # La nueva API puede devolver jobId (mensaje encolado) o id (enviado directamente)
  228. if 'jobId' in response_json:
  229. # Mensaje encolado - si la API devuelve jobId, significa que el mensaje fue aceptado
  230. # y está en proceso de envío, por lo que lo marcamos como 'sent'
  231. job_id = response_json.get('jobId')
  232. _logger.info("Mensaje aceptado por la API. Job ID: %s - Marcando como enviado", job_id)
  233. whatsapp_message.write({
  234. 'state': 'sent', # Marcar como enviado ya que fue aceptado por la API
  235. 'msg_uid': job_id
  236. })
  237. self._cr.commit()
  238. elif 'id' in response_json:
  239. # Mensaje enviado directamente
  240. msg_id = response_json.get('id')
  241. if isinstance(msg_id, dict) and '_serialized' in msg_id:
  242. msg_uid = msg_id['_serialized']
  243. elif isinstance(msg_id, str):
  244. msg_uid = msg_id
  245. else:
  246. msg_uid = str(msg_id)
  247. _logger.info("Mensaje enviado exitosamente. ID: %s", msg_uid)
  248. whatsapp_message.write({
  249. 'state': 'sent',
  250. 'msg_uid': msg_uid
  251. })
  252. self._cr.commit()
  253. else:
  254. _logger.warning("Respuesta exitosa pero sin jobId ni id: %s", response_json)
  255. whatsapp_message.write({
  256. 'state': 'outgoing'
  257. })
  258. self._cr.commit()
  259. except ValueError:
  260. _logger.error("La respuesta no es JSON válido: %s", response.text)
  261. whatsapp_message.write({
  262. 'state': 'error'
  263. })
  264. self._cr.commit()
  265. else:
  266. _logger.error("Error en la petición. Código: %s, Respuesta: %s", response.status_code, response.text)
  267. whatsapp_message.write({
  268. 'state': 'error'
  269. })
  270. self._cr.commit()
  271. except requests.exceptions.RequestException as e:
  272. _logger.error("Error de conexión al enviar mensaje: %s", str(e))
  273. whatsapp_message.write({
  274. 'state': 'error'
  275. })
  276. self._cr.commit()
  277. time.sleep(random.randint(3, 7))