221 lines
8.1 KiB
Python
221 lines
8.1 KiB
Python
# 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
|