""" 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