# communications/api_views.py """API для расширенных функций мессенджера""" from django.contrib.auth.decorators import login_required from django.db import models, transaction from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.utils import timezone from django.views.decorators.http import require_POST from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from .models import ( Chat, ChatMembership, ChatUserSettings, ForwardedMessage, Message, MessageAttachment, MessageReply, ) # ============ CHAT MANAGEMENT ============ @login_required @require_POST def create_chat(request): """ Создание нового чата (group, department, channel, announcement, global) """ import json # Читаем данные из FormData или JSON if request.content_type and 'multipart/form-data' in request.content_type: # FormData (с файлами) chat_type = request.POST.get('type', 'group') name = request.POST.get('name', '').strip() description = request.POST.get('description', '').strip() department_id = request.POST.get('department_id') include_all = request.POST.get('include_all_employees') == 'true' is_main = request.POST.get('is_main') == 'true' avatar = request.FILES.get('avatar') participant_ids = request.POST.getlist('participant_ids') else: # JSON (без файлов - legacy) try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse( {'ok': False, 'error': 'Invalid JSON'}, status=400 ) chat_type = data.get('type', 'group') name = data.get('name', '').strip() description = data.get('description', '').strip() department_id = data.get('department_id') include_all = data.get('include_all_employees', False) is_main = data.get('is_main', False) avatar = None participant_ids = data.get('participant_ids', []) # Валидация типа чата valid_types = ['group', 'department', 'channel', 'announcement', 'global'] if chat_type not in valid_types: types_str = ", ".join(valid_types) return JsonResponse( { 'ok': False, 'error': f'Invalid chat type. Must be one of: {types_str}' }, status=400 ) # Валидация для announcement - только 1 на пользователя if chat_type == 'announcement': existing = Chat.objects.filter( type='announcement', created_by=request.user ).first() if existing: if existing.is_blocked: return JsonResponse( { 'ok': False, 'error': 'Ваш чат объявлений заблокирован администратором' }, status=403 ) else: return JsonResponse( { 'ok': False, 'error': 'У вас уже есть чат объявлений', 'existing_chat_id': existing.id }, status=400 ) # Валидация названия if not name and chat_type != 'private': # Для announcement генерируем название автоматически if chat_type == 'announcement': user_name = request.user.get_full_name() or request.user.username name = f"Объявления от {user_name}" else: return JsonResponse( {'ok': False, 'error': 'Name is required'}, status=400 ) # Валидация для чата отдела if chat_type == 'department': if not department_id: return JsonResponse( { 'ok': False, 'error': 'Department is required for department chat' }, status=400 ) try: from employees.models import Department department = Department.objects.get(id=department_id) except Department.DoesNotExist: return JsonResponse( {'ok': False, 'error': 'Department not found'}, status=400 ) else: department = None # Валидация для основного чата if is_main: if chat_type == 'global': # Проверяем, нет ли уже основного глобального чата if Chat.objects.filter(type='global', is_main=True).exists(): return JsonResponse( {'ok': False, 'error': 'Main global chat already exists'}, status=400 ) elif chat_type == 'department' and department: # Проверяем, нет ли уже основного чата для этого отдела exists = Chat.objects.filter( type='department', department=department, is_main=True ).exists() if exists: return JsonResponse( { 'ok': False, 'error': 'Main department chat already exists' }, status=400 ) # Создаем чат chat = Chat.objects.create( type=chat_type, name=name, description=description, created_by=request.user, department=department, include_all_employees=include_all, is_main=is_main, can_reply=(chat_type != 'announcement') # Для announcement нельзя ) # Добавляем аватар если был загружен if avatar: chat.avatar = avatar chat.save() # Создатель - владелец с полными правами ChatMembership.objects.create( chat=chat, user=request.user, role='owner', can_send_messages=True, can_add_members=True, can_remove_members=True, can_pin_messages=True ) # Добавляем участников для группового чата if chat_type == 'group' and participant_ids: from employees.models import Employee for emp_id in participant_ids: try: employee = Employee.objects.get(id=emp_id) if employee != request.user: # Создателя уже добавили ChatMembership.objects.create( chat=chat, user=employee, role='member', can_send_messages=True ) except Employee.DoesNotExist: pass # Для чатов с include_all_employees - добавляем всех активных if include_all: from employees.models import Employee all_employees = Employee.objects.filter(is_active=True).exclude( id=request.user.id ) for employee in all_employees: # Для announcement и channel - обычные участники не могут писать if chat_type == 'announcement': can_send = False # Только создатель может писать elif chat_type == 'channel': can_send = False # Только модераторы могут писать else: can_send = True ChatMembership.objects.get_or_create( chat=chat, user=employee, defaults={ 'role': 'member', 'can_send_messages': can_send } ) # Для чата отдела - добавляем всех сотрудников отдела if chat_type == 'department' and department: from employees.models import EmployeeDepartment dept_employees = EmployeeDepartment.objects.filter( department=department, is_active=True ).select_related('employee') for ed in dept_employees: if ed.employee != request.user: # Создателя уже добавили ChatMembership.objects.get_or_create( chat=chat, user=ed.employee, defaults={ 'role': 'member', 'can_send_messages': True } ) return JsonResponse({ 'ok': True, 'chat_id': chat.id, 'chat_type': chat.type, 'name': chat.name }) @login_required @require_POST def block_announcement(request, chat_id): """ Блокирует announcement чат (только для staff/superuser) После блокировки чат скрывается у всех пользователей """ if not (request.user.is_staff or request.user.is_superuser): return JsonResponse( {'ok': False, 'error': 'Permission denied'}, status=403 ) chat = get_object_or_404(Chat, pk=chat_id, type='announcement') if chat.is_blocked: return JsonResponse( {'ok': False, 'error': 'Chat already blocked'}, status=400 ) chat.is_blocked = True chat.blocked_at = timezone.now() chat.blocked_by = request.user chat.save() return JsonResponse({ 'ok': True, 'message': f'Announcement "{chat.name}" blocked' }) @login_required @require_POST def unblock_announcement(request, chat_id): """ Разблокирует announcement чат (только для staff/superuser) """ if not (request.user.is_staff or request.user.is_superuser): return JsonResponse( {'ok': False, 'error': 'Permission denied'}, status=403 ) chat = get_object_or_404(Chat, pk=chat_id, type='announcement') if not chat.is_blocked: return JsonResponse( {'ok': False, 'error': 'Chat is not blocked'}, status=400 ) chat.is_blocked = False chat.blocked_at = None chat.blocked_by = None chat.save() return JsonResponse({ 'ok': True, 'message': f'Announcement "{chat.name}" unblocked' }) @login_required @require_POST def update_chat(request, chat_id): """Обновление названия/описания/аватара чата""" chat = get_object_or_404(Chat, pk=chat_id) # Проверка прав (только owner/admin) membership = ChatMembership.objects.filter( chat=chat, user=request.user, role__in=['owner', 'admin'] ).first() if not membership: return JsonResponse({'ok': False, 'error': 'Forbidden'}, status=403) # Обновление полей if 'name' in request.POST: chat.name = request.POST['name'].strip() if 'description' in request.POST: chat.description = request.POST['description'].strip() if 'avatar' in request.FILES: chat.avatar = request.FILES['avatar'] chat.save() return JsonResponse({'ok': True}) # ============ CHAT USER SETTINGS ============ @login_required @require_POST def pin_chat(request, chat_id): """Закрепление/открепление чата""" chat = get_object_or_404(Chat, pk=chat_id) is_pinned = request.POST.get('pinned', 'true').lower() == 'true' settings, created = ChatUserSettings.objects.get_or_create( chat=chat, user=request.user ) settings.is_pinned = is_pinned if is_pinned: settings.pinned_at = timezone.now() # Определяем порядок max_order = ChatUserSettings.objects.filter( user=request.user, is_pinned=True ).aggregate(max_order=models.Max('pin_order'))['max_order'] or 0 settings.pin_order = max_order + 1 else: settings.pinned_at = None settings.pin_order = 0 settings.save() return JsonResponse({'ok': True, 'pinned': is_pinned}) @login_required @require_POST def set_chat_notifications(request, chat_id): """Включение/выключение уведомлений""" chat = get_object_or_404(Chat, pk=chat_id) enabled = request.POST.get('enabled', 'true').lower() == 'true' settings, created = ChatUserSettings.objects.get_or_create( chat=chat, user=request.user ) settings.notifications_enabled = enabled settings.save() return JsonResponse({'ok': True, 'enabled': enabled}) # ============ MESSAGE ACTIONS ============ @login_required @require_POST def edit_message(request, message_id): """Редактирование сообщения""" message = get_object_or_404(Message, pk=message_id) if message.author != request.user: return JsonResponse({'ok': False, 'error': 'Forbidden'}, status=403) new_content = request.POST.get('content', '').strip() if not new_content: return JsonResponse({'ok': False, 'error': 'Empty content'}, status=400) # Сохраняем историю if not message.is_edited: message.edit_history = [] message.edit_history.append({ 'timestamp': timezone.now().isoformat(), 'old_content': message.content }) message.content = new_content message.is_edited = True message.edited_at = timezone.now() message.save() return JsonResponse({'ok': True, 'edited_at': message.edited_at.isoformat()}) @login_required @require_POST def delete_message(request, message_id): """Удаление сообщения""" import logging logger = logging.getLogger(__name__) logger.info( "[DELETE_MESSAGE] API called: message_id=%s, user=%s", message_id, request.user.username ) message = get_object_or_404(Message, pk=message_id) logger.info( "[DELETE_MESSAGE] Message found: chat_id=%s, author=%s", message.chat_id, message.author.username ) # Проверка: автор или модератор/админ чата can_delete = False if message.author == request.user: can_delete = True logger.info("[DELETE_MESSAGE] User is author, can delete") else: membership = ChatMembership.objects.filter( chat=message.chat, user=request.user, role__in=['owner', 'admin', 'moderator'] ).first() if membership: can_delete = True logger.info( "[DELETE_MESSAGE] User has moderator role: %s", membership.role ) if not can_delete: logger.warning( "[DELETE_MESSAGE] Permission denied for user %s", request.user.username ) return JsonResponse({'ok': False, 'error': 'Forbidden'}, status=403) logger.info("[DELETE_MESSAGE] Marking message as deleted") message.is_deleted = True message.deleted_at = timezone.now() message.deleted_by = request.user message.save() logger.info( "[DELETE_MESSAGE] Message deleted successfully: %s", message_id ) # Отправляем WebSocket событие всем участникам чата logger.info( "[DELETE_MESSAGE] Sending WebSocket event to chat group: chat_%s", message.chat_id ) channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( f"chat_{message.chat_id}", { "type": "chat.message_deleted", "chat_id": message.chat_id, "message_id": message_id } ) logger.info( "[DELETE_MESSAGE] WebSocket event sent for message_id=%s", message_id ) return JsonResponse({'ok': True}) @login_required @require_POST def pin_message(request, message_id): """Закрепление сообщения в чате""" message = get_object_or_404(Message, pk=message_id) # Проверка прав membership = ChatMembership.objects.filter( chat=message.chat, user=request.user, can_pin_messages=True ).first() if not membership: return JsonResponse({'ok': False, 'error': 'Forbidden'}, status=403) is_pinned = request.POST.get('pinned', 'true').lower() == 'true' message.is_pinned = is_pinned if is_pinned: message.pinned_by = request.user message.pinned_at = timezone.now() else: message.pinned_by = None message.pinned_at = None message.save() return JsonResponse({'ok': True, 'pinned': is_pinned}) # ============ ATTACHMENTS ============ # Примечание: endpoint для загрузки сообщений с файлами # перенесён в api/v1/communications/views.py @login_required @require_POST def upload_attachment(request): """Загрузка вложения к существующему сообщению""" if 'file' not in request.FILES: return JsonResponse({'ok': False, 'error': 'No file'}, status=400) message_id = request.POST.get('message_id') if not message_id: return JsonResponse( {'ok': False, 'error': 'message_id required'}, status=400 ) message = get_object_or_404(Message, pk=message_id) if message.author != request.user: return JsonResponse({'ok': False, 'error': 'Forbidden'}, status=403) uploaded_file = request.FILES['file'] # Определение типа файла mime_type = uploaded_file.content_type or '' if mime_type.startswith('image/'): file_type = 'image' elif mime_type.startswith('video/'): file_type = 'video' elif mime_type.startswith('audio/'): file_type = 'audio' else: file_type = 'file' attachment = MessageAttachment.objects.create( message=message, file=uploaded_file, file_type=file_type, file_name=uploaded_file.name, file_size=uploaded_file.size, mime_type=mime_type ) message.has_attachments = True message.save() return JsonResponse({ 'ok': True, 'attachment_id': attachment.id, 'file_name': attachment.file_name, 'file_type': attachment.file_type, 'file_url': attachment.file.url }) # ============ FORWARDING ============ @login_required @require_POST def forward_message(request, message_id): """Пересылка сообщения в другой чат""" original = get_object_or_404(Message, pk=message_id) target_chat_id = request.POST.get('target_chat_id') if not target_chat_id: return JsonResponse({'ok': False, 'error': 'target_chat_id required'}, status=400) target_chat = get_object_or_404(Chat, pk=target_chat_id) # Проверка доступа к целевому чату membership = ChatMembership.objects.filter( chat=target_chat, user=request.user, can_send_messages=True ).first() if not membership: return JsonResponse({'ok': False, 'error': 'Cannot send to this chat'}, status=403) with transaction.atomic(): # Создаем новое сообщение forwarded = Message.objects.create( chat=target_chat, author=request.user, content=original.content, is_forwarded=True ) # Определяем оригинальное сообщение в цепочке if hasattr(original, 'forward_info'): original_message = original.forward_info.original_message forward_count = original.forward_info.forward_count + 1 else: original_message = original forward_count = 1 # Создаем информацию о пересылке ForwardedMessage.objects.create( message=forwarded, original_message=original_message, immediate_source=original, original_chat=original.chat, original_author=original.author, forwarded_by=request.user, forward_count=forward_count, preserved_content=original.content, preserved_author_name=original.author.get_full_name() ) return JsonResponse({'ok': True, 'new_message_id': forwarded.id}) # ============ REPLIES ============ @login_required @require_POST def reply_to_message(request, message_id): """Ответ на сообщение""" replied_to = get_object_or_404(Message, pk=message_id) content = request.POST.get('content', '').strip() reply_type = request.POST.get('reply_type', 'inline') if not content: return JsonResponse({'ok': False, 'error': 'Content required'}, status=400) # Создаем сообщение-ответ reply = Message.objects.create( chat=replied_to.chat, author=request.user, content=content, reply_to=replied_to ) # Создаем расширенную информацию об ответе MessageReply.objects.create( message=reply, replied_to=replied_to, is_cross_chat_reply=False, preserved_text=replied_to.content[:200], preserved_author_name=replied_to.author.get_full_name(), reply_type=reply_type ) # Если это тред, обновляем счетчик if reply_type == 'thread': if replied_to.thread_root: root = replied_to.thread_root else: root = replied_to reply.thread_root = root reply.save() root.thread_reply_count += 1 root.save() return JsonResponse({ 'ok': True, 'reply_id': reply.id, 'replied_to_id': replied_to.id }) # ============ CHAT MEMBERSHIP ============ @login_required @require_POST def add_member(request, chat_id): """Добавление участника в чат""" chat = get_object_or_404(Chat, pk=chat_id) user_id = request.POST.get('user_id') if not user_id: return JsonResponse({'ok': False, 'error': 'user_id required'}, status=400) # Проверка прав membership = ChatMembership.objects.filter( chat=chat, user=request.user, can_add_members=True ).first() if not membership: return JsonResponse({'ok': False, 'error': 'Forbidden'}, status=403) from employees.models import Employee new_user = get_object_or_404(Employee, pk=user_id) # Создаем или активируем участие new_membership, created = ChatMembership.objects.get_or_create( chat=chat, user=new_user, defaults={ 'role': 'member', 'invited_by': request.user, 'can_send_messages': True } ) if not created and not new_membership.is_active: new_membership.is_active = True new_membership.left_at = None new_membership.save() return JsonResponse({'ok': True, 'created': created}) @login_required @require_POST def remove_member(request, chat_id): """Удаление участника из чата""" chat = get_object_or_404(Chat, pk=chat_id) user_id = request.POST.get('user_id') if not user_id: return JsonResponse({'ok': False, 'error': 'user_id required'}, status=400) # Проверка прав membership = ChatMembership.objects.filter( chat=chat, user=request.user, can_remove_members=True ).first() if not membership: return JsonResponse({'ok': False, 'error': 'Forbidden'}, status=403) target_membership = ChatMembership.objects.filter( chat=chat, user_id=user_id ).first() if target_membership: target_membership.is_active = False target_membership.left_at = timezone.now() target_membership.save() return JsonResponse({'ok': True})