Chat interno 1-a-1 entre usuarios de la plataforma (addon skeen_chat + pagina Chat con badge de no leidos)
This commit is contained in:
4
odoo-addons/skeen_chat/__init__.py
Normal file
4
odoo-addons/skeen_chat/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
21
odoo-addons/skeen_chat/__manifest__.py
Normal file
21
odoo-addons/skeen_chat/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'SKEEN Chat Interno',
|
||||
'version': '1.0.0',
|
||||
'category': 'Healthcare',
|
||||
'summary': 'Chat interno 1-a-1 entre usuarios del frontend SKEEN',
|
||||
'description': """
|
||||
Mensajería directa entre usuarios de skeen.frontend.user.
|
||||
Sin websockets: el frontend React consulta por polling.
|
||||
""",
|
||||
'author': 'Consultoria Alcaraz Salazar, S.A.S.',
|
||||
'website': 'https://skeen.mx',
|
||||
'depends': ['skeen_whatsapp'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
3
odoo-addons/skeen_chat/controllers/__init__.py
Normal file
3
odoo-addons/skeen_chat/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import chat
|
||||
165
odoo-addons/skeen_chat/controllers/chat.py
Normal file
165
odoo-addons/skeen_chat/controllers/chat.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# -*- 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)
|
||||
3
odoo-addons/skeen_chat/models/__init__.py
Normal file
3
odoo-addons/skeen_chat/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import chat_message
|
||||
22
odoo-addons/skeen_chat/models/chat_message.py
Normal file
22
odoo-addons/skeen_chat/models/chat_message.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class SkeenChatMessage(models.Model):
|
||||
"""Mensaje directo 1-a-1 entre usuarios del frontend React.
|
||||
|
||||
La seguridad real está en el controlador: un usuario solo puede leer o
|
||||
crear mensajes donde él es sender_id o recipient_id.
|
||||
"""
|
||||
_name = 'skeen.chat.message'
|
||||
_description = 'Mensaje de Chat Interno'
|
||||
_order = 'id'
|
||||
|
||||
sender_id = fields.Many2one('skeen.frontend.user', string='Remitente',
|
||||
required=True, index=True, ondelete='cascade')
|
||||
recipient_id = fields.Many2one('skeen.frontend.user', string='Destinatario',
|
||||
required=True, index=True, ondelete='cascade')
|
||||
body = fields.Text(string='Mensaje', required=True)
|
||||
is_read = fields.Boolean(string='Leído', default=False, index=True)
|
||||
# create_date (heredado) = fecha/hora de envío
|
||||
2
odoo-addons/skeen_chat/security/ir.model.access.csv
Normal file
2
odoo-addons/skeen_chat/security/ir.model.access.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_skeen_chat_message_user,Acceso a Mensajes de Chat SKEEN,model_skeen_chat_message,base.group_user,1,1,1,1
|
||||
|
Reference in New Issue
Block a user