415 lines
14 KiB
Python
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
|