Files
ETB/ETB-API/collaboration_war_rooms/consumers.py
Iliyan Angelov 6b247e5b9f Updates
2025-09-19 11:58:53 +03:00

415 lines
14 KiB
Python

"""
WebSocket consumers for real-time chat functionality
"""
import json
import uuid
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from django.utils import timezone
from .models import WarRoom, WarRoomMessage, MessageReaction, ChatCommand
User = get_user_model()
class ChatConsumer(AsyncWebsocketConsumer):
"""WebSocket consumer for real-time chat in war rooms"""
async def connect(self):
"""Connect to WebSocket"""
self.room_id = self.scope['url_route']['kwargs']['room_id']
self.room_group_name = f'chat_{self.room_id}'
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Send room info
room_info = await self.get_room_info()
await self.send(text_data=json.dumps({
'type': 'room_info',
'data': room_info
}))
async def disconnect(self, close_code):
"""Disconnect from WebSocket"""
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
"""Receive message from WebSocket"""
try:
data = json.loads(text_data)
message_type = data.get('type')
if message_type == 'chat_message':
await self.handle_chat_message(data)
elif message_type == 'reaction':
await self.handle_reaction(data)
elif message_type == 'command':
await self.handle_command(data)
elif message_type == 'typing':
await self.handle_typing(data)
else:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Unknown message type'
}))
except json.JSONDecodeError:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Invalid JSON'
}))
except Exception as e:
await self.send(text_data=json.dumps({
'type': 'error',
'message': str(e)
}))
async def handle_chat_message(self, data):
"""Handle chat message"""
content = data.get('content', '').strip()
message_type = data.get('message_type', 'TEXT')
reply_to_id = data.get('reply_to_id')
if not content:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Message content cannot be empty'
}))
return
# Create message
message = await self.create_message(content, message_type, reply_to_id)
if message:
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': await self.serialize_message(message)
}
)
# Check for mentions and send notifications
await self.handle_mentions(message)
# Check for commands
await self.check_for_commands(message)
async def handle_reaction(self, data):
"""Handle message reaction"""
message_id = data.get('message_id')
emoji = data.get('emoji')
action = data.get('action', 'add') # 'add' or 'remove'
if not message_id or not emoji:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Message ID and emoji are required'
}))
return
# Handle reaction
if action == 'add':
reaction = await self.add_reaction(message_id, emoji)
else:
reaction = await self.remove_reaction(message_id, emoji)
if reaction is not False:
# Send reaction update to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'reaction_update',
'message_id': message_id,
'reaction': await self.serialize_reaction(reaction) if reaction else None,
'action': action
}
)
async def handle_command(self, data):
"""Handle ChatOps command"""
message_id = data.get('message_id')
command_text = data.get('command_text')
if not message_id or not command_text:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Message ID and command text are required'
}))
return
# Execute command
result = await self.execute_command(message_id, command_text)
# Send command result to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'command_result',
'message_id': message_id,
'result': result
}
)
async def handle_typing(self, data):
"""Handle typing indicator"""
is_typing = data.get('is_typing', False)
# Send typing indicator to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'typing_indicator',
'user': await self.get_user_info(),
'is_typing': is_typing
}
)
async def chat_message(self, event):
"""Send chat message to WebSocket"""
await self.send(text_data=json.dumps({
'type': 'chat_message',
'data': event['message']
}))
async def reaction_update(self, event):
"""Send reaction update to WebSocket"""
await self.send(text_data=json.dumps({
'type': 'reaction_update',
'data': {
'message_id': event['message_id'],
'reaction': event['reaction'],
'action': event['action']
}
}))
async def command_result(self, event):
"""Send command result to WebSocket"""
await self.send(text_data=json.dumps({
'type': 'command_result',
'data': {
'message_id': event['message_id'],
'result': event['result']
}
}))
async def typing_indicator(self, event):
"""Send typing indicator to WebSocket"""
await self.send(text_data=json.dumps({
'type': 'typing_indicator',
'data': {
'user': event['user'],
'is_typing': event['is_typing']
}
}))
@database_sync_to_async
def get_room_info(self):
"""Get room information"""
try:
room = WarRoom.objects.get(id=self.room_id)
return {
'id': str(room.id),
'name': room.name,
'incident_id': str(room.incident.id),
'incident_title': room.incident.title,
'participant_count': room.allowed_users.count(),
'message_count': room.message_count
}
except WarRoom.DoesNotExist:
return None
@database_sync_to_async
def get_user_info(self):
"""Get current user information"""
user = self.scope['user']
if user.is_authenticated:
return {
'id': str(user.id),
'username': user.username,
'display_name': getattr(user, 'display_name', user.username)
}
return None
@database_sync_to_async
def create_message(self, content, message_type, reply_to_id=None):
"""Create a new message"""
try:
room = WarRoom.objects.get(id=self.room_id)
user = self.scope['user']
if not user.is_authenticated:
return None
# Check if user has access to room
if not room.can_user_access(user):
return None
reply_to = None
if reply_to_id:
try:
reply_to = WarRoomMessage.objects.get(id=reply_to_id)
except WarRoomMessage.DoesNotExist:
pass
message = WarRoomMessage.objects.create(
war_room=room,
content=content,
message_type=message_type,
sender=user,
sender_name=user.username,
reply_to=reply_to
)
# Update room message count
room.message_count += 1
room.last_activity = timezone.now()
room.save(update_fields=['message_count', 'last_activity'])
return message
except WarRoom.DoesNotExist:
return None
@database_sync_to_async
def serialize_message(self, message):
"""Serialize message for WebSocket"""
return {
'id': str(message.id),
'content': message.content,
'message_type': message.message_type,
'sender': {
'id': str(message.sender.id) if message.sender else None,
'username': message.sender.username if message.sender else None,
'display_name': message.sender_name
},
'is_pinned': message.is_pinned,
'reply_to_id': str(message.reply_to.id) if message.reply_to else None,
'created_at': message.created_at.isoformat(),
'reactions': list(message.get_reactions_summary())
}
@database_sync_to_async
def add_reaction(self, message_id, emoji):
"""Add reaction to message"""
try:
message = WarRoomMessage.objects.get(id=message_id)
user = self.scope['user']
if not user.is_authenticated:
return False
reaction = message.add_reaction(user, emoji)
return reaction
except WarRoomMessage.DoesNotExist:
return False
@database_sync_to_async
def remove_reaction(self, message_id, emoji):
"""Remove reaction from message"""
try:
message = WarRoomMessage.objects.get(id=message_id)
user = self.scope['user']
if not user.is_authenticated:
return False
message.remove_reaction(user, emoji)
return True
except WarRoomMessage.DoesNotExist:
return False
@database_sync_to_async
def serialize_reaction(self, reaction):
"""Serialize reaction for WebSocket"""
return {
'id': str(reaction.id),
'emoji': reaction.emoji,
'user': {
'id': str(reaction.user.id),
'username': reaction.user.username
},
'created_at': reaction.created_at.isoformat()
}
@database_sync_to_async
def execute_command(self, message_id, command_text):
"""Execute ChatOps command"""
try:
message = WarRoomMessage.objects.get(id=message_id)
user = self.scope['user']
if not user.is_authenticated:
return {'error': 'Authentication required'}
# Parse command
command_type = self._parse_command_type(command_text)
parameters = self._parse_command_parameters(command_text)
# Create chat command
chat_command = ChatCommand.objects.create(
message=message,
command_type=command_type,
command_text=command_text,
parameters=parameters
)
# Execute command
result = chat_command.execute_command(user)
return result
except WarRoomMessage.DoesNotExist:
return {'error': 'Message not found'}
def _parse_command_type(self, command_text):
"""Parse command type from command text"""
command_text = command_text.lower().strip()
if command_text.startswith('/status'):
return 'STATUS'
elif command_text.startswith('/runbook'):
return 'RUNBOOK'
elif command_text.startswith('/escalate'):
return 'ESCALATE'
elif command_text.startswith('/assign'):
return 'ASSIGN'
elif command_text.startswith('/update'):
return 'UPDATE'
else:
return 'CUSTOM'
def _parse_command_parameters(self, command_text):
"""Parse command parameters from command text"""
parts = command_text.split()
if len(parts) > 1:
return {'args': parts[1:]}
return {}
@database_sync_to_async
def handle_mentions(self, message):
"""Handle user mentions in message"""
# This would integrate with notification system
# For now, just a placeholder
pass
@database_sync_to_async
def check_for_commands(self, message):
"""Check if message contains commands"""
# This would check for command patterns and execute them
# For now, just a placeholder
pass