180 lines
6.8 KiB
Python
180 lines
6.8 KiB
Python
# communications/serialization.py
|
||
"""
|
||
Утилиты сериализации для модуля communications.
|
||
Вынесены из consumers.py для переиспользования в API, WebSocket и других местах.
|
||
"""
|
||
from django.urls import reverse
|
||
|
||
|
||
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": f"/api/v1/employees/{author.id}/" if author else "",
|
||
"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 as e:
|
||
# Если не удалось загрузить 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
|