Corp/backend/communications/MESSENGER_IMPROVEMENTS.md
Игорь Надеин ddf22c47da fix: автоматическое обновление JWT токенов
- ApiClient теперь обрабатывает 401 и 403 ошибки для обновления токена
- Добавлен JWTRefreshMiddleware для проактивного обновления токенов за 5 минут до истечения
- Добавлена настройка JWT_REFRESH_THRESHOLD_MINUTES (по умолчанию 5)
- Исправлена проблема с периодическими 403 ошибками после истечения access token
2025-11-20 18:52:29 +03:00

866 lines
32 KiB
Markdown
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.

# Улучшения модели мессенджера
## 1. Добавить базовые поля в модель Chat
```python
class Chat(models.Model):
# Существующие поля...
type = models.CharField(max_length=16, choices=CHAT_TYPE_CHOICES)
# НОВЫЕ ПОЛЯ:
name = models.CharField(
max_length=255,
blank=True,
verbose_name="Название чата",
help_text="Для групповых/канальных чатов"
)
description = models.TextField(
blank=True,
verbose_name="Описание чата"
)
avatar = models.ImageField(
upload_to='chat_avatars/%Y/%m/',
null=True,
blank=True,
verbose_name="Аватар чата"
)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
on_delete=models.SET_NULL,
related_name='created_chats',
verbose_name="Создатель"
)
# Для гибкой настройки участников
include_all_employees = models.BooleanField(
default=False,
verbose_name="Включить всех сотрудников",
help_text="Для анонсов и общих чатов"
)
```
## 2. Создать модель для закрепления чатов (персональная настройка)
```python
class ChatUserSettings(models.Model):
"""Персональные настройки пользователя для чата"""
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='user_settings')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
# Закрепление
is_pinned = models.BooleanField(default=False, verbose_name="Закреплен")
pinned_at = models.DateTimeField(null=True, blank=True, verbose_name="Время закрепления")
pin_order = models.IntegerField(default=0, verbose_name="Порядок закрепленных")
# Уведомления
notifications_enabled = models.BooleanField(default=True, verbose_name="Уведомления")
# Кастомное название (если пользователь переименовал)
custom_name = models.CharField(max_length=255, blank=True, verbose_name="Свое название")
# Скрыть чат
is_hidden = models.BooleanField(default=False, verbose_name="Скрыт")
class Meta:
unique_together = [('chat', 'user')]
ordering = ['-is_pinned', '-pinned_at']
```
## 3. Расширить типы чатов
```python
class Chat(models.Model):
CHAT_TYPE_CHOICES = [
('private', 'Личный'), # 1-на-1
('group', 'Групповой'), # Произвольная группа сотрудников
('department', 'Отдел'), # Привязан к отделу
('channel', 'Канал'), # Только админы пишут, все читают
('announcement', 'Объявления'), # Системный канал
]
```
## 4. Добавить роли участников
```python
class ChatMembership(models.Model):
"""Участие в чате с ролями"""
ROLE_CHOICES = [
('owner', 'Владелец'),
('admin', 'Администратор'),
('moderator', 'Модератор'),
('member', 'Участник'),
('readonly', 'Только чтение'),
]
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='memberships')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='member')
joined_at = models.DateTimeField(auto_now_add=True)
invited_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
on_delete=models.SET_NULL,
related_name='+'
)
# Разрешения
can_send_messages = models.BooleanField(default=True)
can_add_members = models.BooleanField(default=False)
can_edit_chat = models.BooleanField(default=False)
class Meta:
unique_together = [('chat', 'user')]
```
## 5. Расширить модель Message
```python
class Message(models.Model):
# Существующие поля...
chat = models.ForeignKey(Chat, related_name="messages", on_delete=models.CASCADE)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
# НОВЫЕ ПОЛЯ:
# Ответ на сообщение (упрощенная связь, детали в MessageReply)
reply_to = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='direct_replies',
help_text="Простая связь для быстрого доступа"
)
# Редактирование
is_edited = models.BooleanField(default=False)
edited_at = models.DateTimeField(null=True, blank=True)
edit_history = models.JSONField(
default=list,
blank=True,
help_text="История редактирования [{timestamp, old_content}, ...]"
)
# Удаление
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
deleted_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text="Кто удалил (может отличаться от автора)"
)
# Вложения
has_attachments = models.BooleanField(default=False)
# Системные сообщения (вступление, выход и т.д.)
is_system = models.BooleanField(default=False)
system_type = models.CharField(max_length=50, blank=True)
# Типы: 'user_joined', 'user_left', 'chat_created', 'chat_renamed',
# 'user_promoted', 'user_demoted', 'settings_changed'
system_metadata = models.JSONField(
default=dict,
blank=True,
help_text="Дополнительные данные для системных сообщений"
)
# Важные/закрепленные
is_pinned = models.BooleanField(default=False)
pinned_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
pinned_at = models.DateTimeField(null=True, blank=True)
# Реакции (можно хранить как JSON)
reactions = models.JSONField(default=dict, blank=True)
# Формат: {'👍': [user_id1, user_id2], '❤️': [user_id3]}
# Флаги для специальных типов сообщений
is_forwarded = models.BooleanField(default=False)
is_cross_chat = models.BooleanField(
default=False,
help_text="Сообщение отправлено не участником чата"
)
# Треды (для организации обсуждений)
thread_root = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='thread_messages',
help_text="Корневое сообщение треда"
)
thread_reply_count = models.IntegerField(default=0)
class Meta:
ordering = ['created_at']
indexes = [
models.Index(fields=['chat', 'created_at']),
models.Index(fields=['chat', 'is_deleted', 'created_at']),
models.Index(fields=['author', 'created_at']),
models.Index(fields=['thread_root', 'created_at']),
models.Index(fields=['is_pinned', 'chat']),
]
```
## 6. Добавить модель вложений
```python
class MessageAttachment(models.Model):
ATTACHMENT_TYPE_CHOICES = [
('image', 'Изображение'),
('file', 'Файл'),
('video', 'Видео'),
('audio', 'Аудио'),
]
message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='attachments')
file = models.FileField(upload_to='chat_attachments/%Y/%m/%d/')
file_type = models.CharField(max_length=20, choices=ATTACHMENT_TYPE_CHOICES)
file_name = models.CharField(max_length=255)
file_size = models.BigIntegerField()
mime_type = models.CharField(max_length=100)
uploaded_at = models.DateTimeField(auto_now_add=True)
# Для изображений
thumbnail = models.ImageField(upload_to='chat_thumbnails/%Y/%m/%d/', null=True, blank=True)
```
## 6a. Улучшить модель пересылки сообщений
```python
class ForwardedMessage(models.Model):
"""Информация о пересланных сообщениях (поддержка цепочки пересылок)"""
message = models.OneToOneField(
Message,
on_delete=models.CASCADE,
related_name='forward_info',
help_text="Текущее пересланное сообщение"
)
# Оригинальное сообщение (первое в цепочке)
original_message = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
related_name='forwarded_copies',
help_text="Самое первое сообщение в цепочке пересылок"
)
# Непосредственный источник (откуда переслали)
immediate_source = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
related_name='+',
help_text="Сообщение, которое было непосредственно переслано"
)
original_chat = models.ForeignKey(
Chat,
on_delete=models.SET_NULL,
null=True,
related_name='+'
)
original_author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name='+'
)
forwarded_at = models.DateTimeField(auto_now_add=True)
forwarded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='forwarded_messages',
help_text="Кто переслал"
)
# Счетчик пересылок (для оригинала)
forward_count = models.IntegerField(default=0, help_text="Сколько раз переслали")
# Сохранить контент на случай удаления оригинала
preserved_content = models.TextField(blank=True)
preserved_author_name = models.CharField(max_length=255, blank=True)
class Meta:
indexes = [
models.Index(fields=['original_message']),
models.Index(fields=['forwarded_by', 'forwarded_at']),
]
```
## 6b. Добавить модель для отправки сообщений в чужие чаты
```python
class CrossChatMessage(models.Model):
"""Сообщения, отправленные в чаты, где пользователь не является участником"""
message = models.OneToOneField(
Message,
on_delete=models.CASCADE,
related_name='cross_chat_info'
)
sender = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='cross_chat_messages',
help_text="Отправитель (не член этого чата)"
)
target_chat = models.ForeignKey(
Chat,
on_delete=models.CASCADE,
related_name='cross_messages',
help_text="Чат-получатель"
)
# Разрешение на отправку
approved = models.BooleanField(default=False, help_text="Одобрено модератором")
approved_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text="Кто одобрил"
)
approved_at = models.DateTimeField(null=True, blank=True)
# Причина/контекст отправки
reason = models.CharField(
max_length=500,
blank=True,
help_text="Причина отправки сообщения в чужой чат"
)
# Режим отображения
DISPLAY_MODE_CHOICES = [
('guest', 'Гостевое (с пометкой "Внешнее сообщение")'),
('forwarded', 'Как пересланное'),
('announcement', 'Как объявление'),
]
display_mode = models.CharField(
max_length=20,
choices=DISPLAY_MODE_CHOICES,
default='guest'
)
sent_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['target_chat', 'approved']),
models.Index(fields=['sender', 'sent_at']),
]
```
## 7. Улучшить ChatReadState
```python
class ChatReadState(models.Model):
chat = models.ForeignKey("Chat", on_delete=models.CASCADE, related_name="read_states")
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
last_read_at = models.DateTimeField(null=True, blank=True)
last_read_message = models.ForeignKey(
Message,
null=True,
blank=True,
on_delete=models.SET_NULL,
help_text="Последнее прочитанное сообщение"
)
# Статус набора текста
is_typing = models.BooleanField(default=False)
typing_updated_at = models.DateTimeField(null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True)
```
## 8. Добавить модель для ответов на сообщения (расширенная)
```python
class MessageReply(models.Model):
"""Дополнительная информация об ответе на сообщение"""
message = models.OneToOneField(
Message,
on_delete=models.CASCADE,
related_name='reply_info',
help_text="Сообщение-ответ"
)
replied_to = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
related_name='replies',
help_text="Исходное сообщение, на которое отвечают"
)
# Для ответов через чаты (если отвечаешь в другом чате)
is_cross_chat_reply = models.BooleanField(default=False)
original_chat = models.ForeignKey(
Chat,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text="Чат оригинального сообщения (если отличается)"
)
# Сохраненный контент на случай удаления
preserved_text = models.TextField(blank=True)
preserved_author_name = models.CharField(max_length=255, blank=True)
# Тип ответа
REPLY_TYPE_CHOICES = [
('inline', 'Обычный ответ'),
('quote', 'Цитирование'),
('thread', 'В треде'),
]
reply_type = models.CharField(
max_length=20,
choices=REPLY_TYPE_CHOICES,
default='inline'
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['replied_to']),
]
```
## 9. Добавить права доступа к чатам
```python
class ChatAccessPermission(models.Model):
"""Права пользователей на отправку сообщений в чужие чаты"""
chat = models.ForeignKey(
Chat,
on_delete=models.CASCADE,
related_name='access_permissions'
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='chat_permissions'
)
# Типы доступа
PERMISSION_TYPE_CHOICES = [
('send_messages', 'Отправка сообщений'),
('read_only', 'Только чтение'),
('send_with_approval', 'Отправка с модерацией'),
]
permission_type = models.CharField(
max_length=30,
choices=PERMISSION_TYPE_CHOICES,
default='send_with_approval'
)
# Кто выдал право
granted_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name='+'
)
granted_at = models.DateTimeField(auto_now_add=True)
# Срок действия
expires_at = models.DateTimeField(
null=True,
blank=True,
help_text="Когда истекает право доступа"
)
is_active = models.BooleanField(default=True)
class Meta:
unique_together = [('chat', 'user')]
indexes = [
models.Index(fields=['chat', 'is_active']),
models.Index(fields=['user', 'is_active']),
]
```
## 9. Убрать ограничения для гибкости
Удалить constraint'ы:
- `unique_main_global_chat`
- `unique_main_department_chat`
Заменить на поле `is_default` для автоматического выбора чата по умолчанию.
## 10. Добавить настройки чата
```python
class ChatSettings(models.Model):
"""Глобальные настройки чата"""
chat = models.OneToOneField(Chat, on_delete=models.CASCADE, related_name='settings')
# Кто может писать
only_admins_can_post = models.BooleanField(default=False)
# Кто может приглашать
members_can_invite = models.BooleanField(default=True)
# Автоудаление сообщений
auto_delete_messages_after = models.IntegerField(
null=True,
blank=True,
help_text="Автоматически удалять сообщения старше N дней"
)
# Максимум участников
max_members = models.IntegerField(null=True, blank=True)
# Запретить личные сообщения между участниками
disable_dm_between_members = models.BooleanField(default=False)
```
## Пример миграции (поэтапно)
### Этап 1: Базовые поля
```bash
# Добавить name, description, avatar в Chat
python manage.py makemigrations
python manage.py migrate
```
### Этап 2: Персональные настройки
```bash
# Создать ChatUserSettings
python manage.py makemigrations
python manage.py migrate
```
### Этап 3: Роли участников
```bash
# Создать ChatMembership
python manage.py makemigrations
python manage.py migrate
```
### Этап 4: Расширенные сообщения
```bash
# Добавить поля в Message, создать MessageAttachment
python manage.py makemigrations
python manage.py migrate
```
## Итоговая архитектура
```
Chat (базовая информация + настройки)
├── ChatUserSettings (персональные настройки каждого пользователя)
├── ChatMembership (роли и права участников)
├── ChatSettings (глобальные настройки чата)
├── ChatAccessPermission (права внешних пользователей на отправку сообщений)
├── Message
│ ├── MessageAttachment (файлы, изображения)
│ ├── ForwardedMessage (информация о пересылке с цепочкой)
│ ├── MessageReply (расширенная информация об ответах)
│ └── CrossChatMessage (сообщения из чужих чатов)
└── ChatReadState (статус прочтения)
```
## Новые возможности
### 1. Пересылка сообщений
- **Простая пересылка**: отправить сообщение в другой чат с указанием источника
- **Цепочка пересылок**: отслеживание всей истории пересылки (оригинал → промежуточные → текущее)
- **Счетчик пересылок**: сколько раз сообщение было переслано
- **Сохранение контента**: если оригинал удален, контент сохраняется
### 2. Ответы на сообщения
- **Внутри чата**: стандартный reply с цитированием
- **Кросс-чат ответы**: ответить на сообщение из другого чата
- **Типы ответов**: inline (обычный), quote (цитата), thread (тред)
- **Сохранение контекста**: текст и автор сохраняются при удалении оригинала
### 3. Отправка в чужие чаты
- **Режимы доступа**:
- `send_messages` - свободная отправка
- `read_only` - только чтение
- `send_with_approval` - модерация перед публикацией
- **Типы отображения**:
- `guest` - с пометкой "Внешнее сообщение"
- `forwarded` - как пересланное
- `announcement` - как объявление
- **Права доступа**: временные/постоянные, с указанием срока действия
- **Модерация**: одобрение администраторами чата
### 4. Треды (обсуждения)
- Организация дискуссий в отдельные ветки
- Счетчик ответов в треде
- Корневое сообщение треда
## Примеры использования
### Пересылка сообщения
```python
# В views или consumers
def forward_message(original_message_id, target_chat_id, user):
original = Message.objects.get(id=original_message_id)
# Создаем новое сообщение
forwarded_msg = Message.objects.create(
chat_id=target_chat_id,
author=user,
content=original.content,
is_forwarded=True,
has_attachments=original.has_attachments
)
# Копируем вложения
for att in original.attachments.all():
MessageAttachment.objects.create(
message=forwarded_msg,
file=att.file,
file_type=att.file_type,
file_name=att.file_name,
file_size=att.file_size,
mime_type=att.mime_type
)
# Находим оригинал в цепочке
if hasattr(original, 'forward_info'):
# Это уже пересланное - берем самый первый оригинал
root_original = original.forward_info.original_message
else:
root_original = original
# Создаем информацию о пересылке
ForwardedMessage.objects.create(
message=forwarded_msg,
original_message=root_original,
immediate_source=original,
original_chat=root_original.chat,
original_author=root_original.author,
forwarded_by=user,
preserved_content=root_original.content,
preserved_author_name=root_original.author.get_full_name()
)
# Увеличиваем счетчик
if hasattr(root_original, 'forward_info'):
root_original.forward_info.forward_count += 1
root_original.forward_info.save()
return forwarded_msg
```
### Отправка в чужой чат
```python
def send_to_external_chat(sender, chat_id, content, reason=''):
chat = Chat.objects.get(id=chat_id)
# Проверяем права доступа
permission = ChatAccessPermission.objects.filter(
chat=chat,
user=sender,
is_active=True
).first()
if not permission:
# Создаем запрос на отправку с модерацией
permission = ChatAccessPermission.objects.create(
chat=chat,
user=sender,
permission_type='send_with_approval',
granted_by=None
)
# Создаем сообщение
message = Message.objects.create(
chat=chat,
author=sender,
content=content,
is_cross_chat=True
)
# Создаем метаданные
CrossChatMessage.objects.create(
message=message,
sender=sender,
target_chat=chat,
approved=(permission.permission_type == 'send_messages'),
reason=reason,
display_mode='guest'
)
return message
```
### Ответ на сообщение из другого чата
```python
def reply_across_chats(user, original_msg_id, reply_content, target_chat_id):
original_msg = Message.objects.get(id=original_msg_id)
# Создаем ответ в целевом чате
reply_msg = Message.objects.create(
chat_id=target_chat_id,
author=user,
content=reply_content,
reply_to=original_msg
)
# Создаем расширенную информацию
MessageReply.objects.create(
message=reply_msg,
replied_to=original_msg,
is_cross_chat_reply=True,
original_chat=original_msg.chat,
preserved_text=original_msg.content,
preserved_author_name=original_msg.author.get_full_name(),
reply_type='quote'
)
return reply_msg
```
## Приоритет внедрения (для MVP)
1. **Высокий приоритет:**
- `name`, `avatar` в Chat
- `ChatUserSettings` (закрепление, уведомления)
- Расширение типов чатов (`group`, `channel`)
- `reply_to`, `is_edited`, `is_forwarded`, `is_cross_chat` в Message
- `MessageAttachment`
- `ForwardedMessage` (базовая пересылка)
- `MessageReply` (ответы в том же чате)
2. **Средний приоритет:**
- `ChatMembership` (роли)
- `ChatAccessPermission` (права на отправку в чужие чаты)
- `CrossChatMessage` (отправка в чужие чаты с модерацией)
- Кросс-чат ответы
- Реакции на сообщения
- Системные сообщения
- История редактирования
3. **Низкий приоритет:**
- Треды
- Автоудаление
- Расширенные разрешения
- Статус "печатает"
- Счетчики пересылок
## Изменения в UI/UX
После внедрения этих улучшений понадобится:
### Основные функции
- Модалка редактирования чата (название, аватар, описание)
- Контекстное меню для закрепления чатов
- Управление участниками (добавить/удалить/изменить роль)
- Загрузка файлов в сообщениях
- Ответы на сообщения (quote UI)
- Реакции на сообщения
- Редактирование/удаление своих сообщений
### Пересылка сообщений
- Кнопка "Переслать" в контекстном меню сообщения
- Модалка выбора чатов для пересылки (с поиском)
- Множественная пересылка (выбрать несколько чатов)
- Отображение "Переслано от [Имя]" в сообщении
- Ссылка на оригинальное сообщение (если доступно)
- Индикатор цепочки пересылок ("Переслано 5 раз")
### Ответы между чатами
- Возможность ответить в другом чате через контекстное меню
- Выбор целевого чата для ответа
- Цитата с указанием чата-источника
- Ссылка "Перейти к оригиналу" (если есть доступ)
### Отправка в чужие чаты
- Модалка "Отправить сообщение в чат"
- Поиск доступных чатов (даже без участия)
- Выбор режима отправки (гостевое/пересланное/объявление)
- Поле "Причина отправки"
- Статус модерации:
- "Ожидает одобрения" (желтый значок)
- "Одобрено" (зеленая галочка)
- "Отклонено" (красный крестик)
- Для модераторов: панель одобрения внешних сообщений
### Визуальные индикаторы
- **Пересланное сообщение**:
```
┌─────────────────────────────────┐
│ 📤 Переслано от Иван Петров │
│ Оригинал: Чат отдела IT │
├─────────────────────────────────┤
│ Текст сообщения... │
└─────────────────────────────────┘
```
- **Внешнее сообщение**:
```
┌─────────────────────────────────┐
│ 🔔 Внешнее сообщение │
│ От: Мария Сидорова │
│ Причина: Важное объявление │
├─────────────────────────────────┤
│ Текст сообщения... │
└─────────────────────────────────┘
```
- **Ответ из другого чата**:
```
┌─────────────────────────────────┐
│ Иван Петров → Чат отдела IT │
│ ↳ В ответ на сообщение из │
│ "Общий чат компании" │
├─────────────────────────────────┤
│ > Исходное сообщение... │
│ Ответ... │
└─────────────────────────────────┘
```
### Контекстное меню сообщения (расширенное)
- Ответить
- Ответить в другом чате →
- Переслать →
- В избранное
- В другой чат...
- В несколько чатов...
- Отправить в чужой чат →
- Скопировать
- Редактировать (свои)
- Удалить (свои/админ)
- Закрепить (админ)
- Пожаловаться
### Права и разрешения (админ панель чата)
- Настройки чата:
- Кто может писать (все/только участники/только админы)
- Разрешить внешние сообщения (да/нет/с модерацией)
- Кто может приглашать участников
- Управление внешними отправителями:
- Список пользователей с правами
- Добавить/удалить права
- Установить срок действия
- Модерация ожидающих сообщений