Corp/backend/communications/api_views.py.backup

765 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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})