# communications/serialization.py """ Утилиты сериализации для модуля communications. Вынесены из consumers.py для переиспользования в API, WebSocket и других местах. """ from django.conf import settings def _get_author_url(author) -> str: """ Получает URL профиля автора используя настройку из settings. Settings: COMMUNICATIONS_AUTHOR_URL_PATTERN (str): Шаблон URL для профиля автора. По умолчанию: '/api/v1/employees/{id}/' (проектно-специфичный) Для standalone: '/users/{id}/' или None (пустая строка) Должен содержать placeholder {id} для подстановки ID пользователя Args: author: Объект пользователя (User model) Returns: str: URL профиля автора или пустая строка Examples: >>> # settings.py >>> COMMUNICATIONS_AUTHOR_URL_PATTERN = '/users/{id}/' >>> _get_author_url(user) '/users/123/' """ if not author: return "" url_pattern = getattr( settings, 'COMMUNICATIONS_AUTHOR_URL_PATTERN', '/api/v1/employees/{id}/' # backward compatibility ) if url_pattern is None: return "" try: return url_pattern.format(id=author.id) except (KeyError, ValueError, AttributeError): return "" def serialize_message(m) -> dict: """ Сериализация сообщения с поддержкой всех полей. Поддерживаемые фичи: - Базовые поля (контент, автор, дата) - Редактирование и удаление - Пересылка сообщений (forward) - Реакции - Вложения (attachments) - Голосования (polls) - Ответы на сообщения (reply_to) """ author = m.author author_name = author.get_full_name() or author.username avatar = "" try: if getattr(author, "avatar", None) and author.avatar: avatar = author.avatar.url except Exception: avatar = "" # Базовые поля data = { "id": m.id, "content": m.content, "author_id": author.id if author else None, "author_name": author_name, "author_url": _get_author_url(author), "avatar": avatar, "created": m.created_at.strftime("%d.%m.%Y %H:%M"), "created_ts": int(m.created_at.timestamp() * 1000), # Статусные поля "is_edited": m.is_edited, "edited_at": m.edited_at.isoformat() if m.edited_at else None, "is_deleted": m.is_deleted, "is_pinned": m.is_pinned, "is_forwarded": m.is_forwarded, "is_system": m.is_system, "has_attachments": m.has_attachments, } # Информация о пересылке (используем forward_metadata) if m.is_forwarded: try: metadata = m.forward_metadata forwarded_data = { "author_id": metadata.original_author.id if metadata.original_author else None, "author_name": ( metadata.original_author.get_full_name() if metadata.original_author else metadata.original_author.username if metadata.original_author else "Неизвестно" ), "message_id": metadata.original_message_id if metadata.original_message else None, } # Добавляем дату оригинального сообщения if metadata.original_created_at: forwarded_data["created_at"] = ( metadata.original_created_at.strftime("%d.%m.%Y %H:%M") ) forwarded_data["created_ts"] = int( metadata.original_created_at.timestamp() * 1000 ) # Добавляем название исходного чата if metadata.original_chat_name: forwarded_data["chat_name"] = metadata.original_chat_name data["forwarded_from"] = forwarded_data except Exception: # Если метаданных нет, просто не добавляем информацию о пересылке pass # Реакции - сериализуем из связанной модели MessageReaction reactions_summary = {} for reaction in m.reactions.select_related('user'): emoji = reaction.emoji if emoji not in reactions_summary: reactions_summary[emoji] = { 'count': 0, 'users': [], 'user_names': [] } reactions_summary[emoji]['count'] += 1 reactions_summary[emoji]['users'].append(reaction.user_id) reactions_summary[emoji]['user_names'].append( reaction.user.get_full_name() or reaction.user.username ) data["reactions_summary"] = reactions_summary # Вложения - всегда включаем поле attachments attachments = [] if m.has_attachments: for att in m.attachments.all(): attachments.append({ "id": att.id, "file_name": att.file_name, "file_type": att.file_type, "file_url": att.file.url, "file_size": att.file_size, "mime_type": att.mime_type, "width": att.width, # Размеры для CSS aspect-ratio "height": att.height, "thumbnail": ( att.thumbnail.url if getattr(att, "thumbnail", None) else None ), }) data["attachments"] = attachments # Голосование if hasattr(m, 'poll'): poll = m.poll poll_data = { "id": poll.id, "question": poll.question, "is_anonymous": poll.is_anonymous, "is_multiple_choice": poll.is_multiple_choice, "is_quiz": poll.is_quiz, "is_closed": poll.is_closed, "closes_at": poll.closes_at.isoformat() if poll.closes_at else None, "total_voters": poll.total_voters, "options": [] } for option in poll.options.all(): poll_data["options"].append({ "id": option.id, "text": option.text, "position": option.position, "vote_count": option.vote_count, "percentage": 0 # Будет пересчитан на клиенте }) data["poll"] = poll_data # Ответ на сообщение if m.reply_to_id: try: reply_msg = m.reply_to if hasattr(m, 'reply_to') else None if not reply_msg: from communications.models import Message as Msg reply_msg = Msg.objects.select_related('author').get( pk=m.reply_to_id ) data["reply_to"] = { "id": reply_msg.id, "content": ( reply_msg.content[:100] if reply_msg.content else "" ), "author_name": ( reply_msg.author.get_full_name() if reply_msg.author else "Неизвестный" ) } except Exception: # Если не удалось загрузить reply_to, просто пропускаем pass # Информация о пересылке (legacy) if m.is_forwarded and hasattr(m, 'forward_info'): fw = m.forward_info data["forward_info"] = { "original_author": fw.preserved_author_name, "forward_count": fw.forward_count, } return data