- ApiClient теперь обрабатывает 401 и 403 ошибки для обновления токена - Добавлен JWTRefreshMiddleware для проактивного обновления токенов за 5 минут до истечения - Добавлена настройка JWT_REFRESH_THRESHOLD_MINUTES (по умолчанию 5) - Исправлена проблема с периодическими 403 ошибками после истечения access token
866 lines
32 KiB
Markdown
866 lines
32 KiB
Markdown
# Улучшения модели мессенджера
|
||
|
||
## 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 │
|
||
│ ↳ В ответ на сообщение из │
|
||
│ "Общий чат компании" │
|
||
├─────────────────────────────────┤
|
||
│ > Исходное сообщение... │
|
||
│ Ответ... │
|
||
└─────────────────────────────────┘
|
||
```
|
||
|
||
### Контекстное меню сообщения (расширенное)
|
||
- Ответить
|
||
- Ответить в другом чате →
|
||
- Переслать →
|
||
- В избранное
|
||
- В другой чат...
|
||
- В несколько чатов...
|
||
- Отправить в чужой чат →
|
||
- Скопировать
|
||
- Редактировать (свои)
|
||
- Удалить (свои/админ)
|
||
- Закрепить (админ)
|
||
- Пожаловаться
|
||
|
||
### Права и разрешения (админ панель чата)
|
||
- Настройки чата:
|
||
- Кто может писать (все/только участники/только админы)
|
||
- Разрешить внешние сообщения (да/нет/с модерацией)
|
||
- Кто может приглашать участников
|
||
- Управление внешними отправителями:
|
||
- Список пользователей с правами
|
||
- Добавить/удалить права
|
||
- Установить срок действия
|
||
- Модерация ожидающих сообщений
|
||
|