# -*- coding: utf-8 -*- """Chat interno 1-a-1 entre usuarios del frontend React (skeen.frontend.user). Mismo esquema de auth HMAC que el resto de la API. Todos los endpoints están restringidos al usuario autenticado: solo puede listar/leer conversaciones donde él es sender o recipient, y solo puede enviar como él mismo. """ from odoo import http from odoo.http import request from odoo.addons.skeen_whatsapp.controllers.auth import require_role from odoo.addons.skeen_whatsapp.controllers.frontend import json_response, _parse_json_body MAX_BODY = 2000 HISTORY_LIMIT = 50 LIST_SCAN_LIMIT = 500 class SkeenChatController(http.Controller): def _message_to_dict(self, msg, me_id): return { 'id': msg.id, 'sender_id': msg.sender_id.id, 'recipient_id': msg.recipient_id.id, 'body': msg.body, 'is_read': msg.is_read, 'mine': msg.sender_id.id == me_id, 'date': msg.create_date.strftime('%Y-%m-%d %H:%M:%S') if msg.create_date else None, } @http.route('/skeen/frontend/v1/chat/users', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') def chat_users(self, **kw): """Usuarios activos (menos yo) con conteo de no leídos y último mensaje. Orden: primero conversaciones con actividad (más reciente primero), luego el resto alfabéticamente. """ try: me = request.frontend_user User = request.env['skeen.frontend.user'].sudo() Msg = request.env['skeen.chat.message'].sudo() users = User.search([('active', '=', True), ('id', '!=', me.id)], order='name') unread = {} for g in Msg.read_group( [('recipient_id', '=', me.id), ('is_read', '=', False)], ['sender_id'], ['sender_id']): unread[g['sender_id'][0]] = g['sender_id_count'] last = {} recent = Msg.search([ '|', ('sender_id', '=', me.id), ('recipient_id', '=', me.id), ], order='id desc', limit=LIST_SCAN_LIMIT) for m in recent: other_id = m.recipient_id.id if m.sender_id.id == me.id else m.sender_id.id if other_id not in last: last[other_id] = { 'body': m.body or '', 'date': m.create_date.strftime('%Y-%m-%d %H:%M:%S') if m.create_date else None, 'mine': m.sender_id.id == me.id, } # Conversaciones con actividad primero (más reciente arriba) conversations = sorted( (u for u in users if u.id in last), key=lambda u: last[u.id]['date'] or '', reverse=True) rest = [u for u in users if u.id not in last] return json_response({ 'status': 'success', 'users': [{ 'id': u.id, 'name': u.name, 'role': u.role, 'unread': unread.get(u.id, 0), 'last_message': last.get(u.id, {}).get('body'), 'last_date': last.get(u.id, {}).get('date'), 'last_mine': last.get(u.id, {}).get('mine'), } for u in conversations + rest], }) except Exception as e: return json_response({'status': 'error', 'message': str(e)}, 500) @http.route('/skeen/frontend/v1/chat/messages', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') def chat_messages(self, **kw): """Historial con otro usuario (últimos 50; since_id para incremental). Marca como leídos los mensajes recibidos de ese usuario. """ try: me = request.frontend_user data = _parse_json_body() other_id = int(data.get('user_id') or 0) if not other_id or other_id == me.id: return json_response({'status': 'error', 'message': 'user_id inválido'}, 400) User = request.env['skeen.frontend.user'].sudo() if not User.search([('id', '=', other_id), ('active', '=', True)], limit=1): return json_response({'status': 'error', 'message': 'Usuario no encontrado'}, 404) domain = [ '|', '&', ('sender_id', '=', me.id), ('recipient_id', '=', other_id), '&', ('sender_id', '=', other_id), ('recipient_id', '=', me.id), ] since_id = int(data.get('since_id') or 0) if since_id: domain.append(('id', '>', since_id)) Msg = request.env['skeen.chat.message'].sudo() msgs = Msg.search(domain, order='id desc', limit=HISTORY_LIMIT).sorted('id') # Marcar leídos los recibidos en esta conversación incoming = msgs.filtered(lambda m: m.recipient_id.id == me.id and not m.is_read) if incoming: incoming.write({'is_read': True}) return json_response({ 'status': 'success', 'messages': [self._message_to_dict(m, me.id) for m in msgs], }) except Exception as e: return json_response({'status': 'error', 'message': str(e)}, 500) @http.route('/skeen/frontend/v1/chat/send', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') def chat_send(self, **kw): """Enviar mensaje directo a otro usuario.""" try: me = request.frontend_user data = _parse_json_body() other_id = int(data.get('user_id') or 0) body = (data.get('message') or '').strip() if not other_id or other_id == me.id: return json_response({'status': 'error', 'message': 'user_id inválido'}, 400) if not body: return json_response({'status': 'error', 'message': 'El mensaje no puede estar vacío'}, 400) if len(body) > MAX_BODY: return json_response( {'status': 'error', 'message': f'El mensaje excede {MAX_BODY} caracteres'}, 400) User = request.env['skeen.frontend.user'].sudo() if not User.search([('id', '=', other_id), ('active', '=', True)], limit=1): return json_response({'status': 'error', 'message': 'Usuario no encontrado'}, 404) msg = request.env['skeen.chat.message'].sudo().create({ 'sender_id': me.id, 'recipient_id': other_id, 'body': body, }) return json_response({ 'status': 'success', 'message': self._message_to_dict(msg, me.id), }, 201) except Exception as e: return json_response({'status': 'error', 'message': str(e)}, 500) @http.route('/skeen/frontend/v1/chat/unread_count', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) @require_role('lectura') def chat_unread_count(self, **kw): """Total global de mensajes no leídos del usuario (badge del menú).""" try: me = request.frontend_user total = request.env['skeen.chat.message'].sudo().search_count([ ('recipient_id', '=', me.id), ('is_read', '=', False), ]) return json_response({'status': 'success', 'unread': total}) except Exception as e: return json_response({'status': 'error', 'message': str(e)}, 500)