Chat System
Назначение
Единый глобальный список чатов объединяет все типы коммуникаций: личные переписки, групповые чаты, чаты проектов и чаты задач. Все они доступны через GET /api/v1/chats и реал-тайм через WebSocket /ws/chat.
Чат задачи одновременно встроен в карточку задачи и виден в глобальном списке — пользователь не теряет сообщения независимо от того, откуда он открыл чат.
Типы чатов
| Тип | Создаётся | Участники |
|---|---|---|
direct | POST /api/v1/chats, POST /api/v1/users/:id/ping | Два пользователя |
group | POST /api/v1/chats | Произвольный набор пользователей |
project | Автоматически при добавлении первого участника в проект | Команда проекта |
task | Автоматически при открытии задачи | Автор + исполнитель + соисполнители + их руководители |
Глобальный список чатов
GET /api/v1/chats возвращает чаты, отсортированные по последнему сообщению (новые — первые), и direct_contacts — список всех активных сотрудников для личных переписок.
Для обычного пользователя это список доступных ему чатов:
- direct/group, где пользователь участник;
- project/task chats, где пользователь участник, менеджер проекта или член команды проекта.
Для admin, director и пользователей с system.global_read/system.global_admin endpoint возвращает полный список чатов. Это нужно для директорского и админского режима: такие пользователи могут видеть проектные и задачные чаты, даже если не добавлены в chat_participants.
direct_contacts не зависит от проектной или сущностной видимости: сотрудники не скрываются друг от друга. Если личный чат с сотрудником уже существует, элемент содержит поле chat; если переписки ещё нет, chat отсутствует, а frontend может создать её через POST /api/v1/chats с type="direct" и participant_ids=[user_id].
Каждый элемент списка содержит:
{
"id": 12,
"title": "Обсуждение дизайна",
"type": "task",
"task_id": 45,
"project_id": 7,
"last_message": {
"id": 388,
"text": "Файл прикреплён",
"kind": "text",
"user_id": 3,
"user_name": "Анна Смирнова",
"created_at": "2026-06-11T14:22:00Z"
},
"unread_count": 3,
"participant_ids": [1, 3, 8],
"created_by": 1,
"created_at": "2026-05-01T10:00:00Z",
"updated_at": "2026-06-11T14:22:00Z"
}last_message равен null для чатов без сообщений. unread_count — количество чужих сообщений после последнего прочитанного (свои сообщения непрочитанными не считаются). Администратор и глобальное чтение видят все чаты, а счётчик приходит для тех, где они участники.
participant_ids — активные участники чата. Название личного диалога клиент берёт по собеседнику (participant_ids без себя): в title сервер хранит имя, которое видел создатель чата.
Прочитанность сообщений
POST /api/v1/chats/:id/read — пометить чат как прочитанный до указанного сообщения.
{ "message_id": 388 }- Сохраняет
last_read_message_idдля пары(chat_id, user_id). - Курсор движется только вперёд: если прислать
message_idменьше текущего — состояние не изменится. message_idдолжен относиться к этому чату, иначе400.- Событие
chat_readрассылается всем участникам чата через WebSocket — для отображения индикаторов «прочитано». - Уведомления
chat.messageиchat.mentionэтого чата доmessage_idотмечаются прочитанными; в/ws/notificationsуходитnotification_read.
Чат задачи
Открывается через GET /api/v1/tasks/:id/chat — создаётся автоматически при первом обращении.
Состав участников синхронизируется с задачей: при смене исполнителя, добавлении/удалении соисполнителей или изменении руководителя участника задачи состав обновляется автоматически. Удалённый участник немедленно получает событие participant_removed и теряет подписку на WS. Название task-чата равно названию задачи, а название project-чата равно названию проекта.
Endpoints чата задачи:
| Метод | URL | Описание |
|---|---|---|
GET | /api/v1/tasks/:id/chat | Получить/создать чат |
GET | /api/v1/tasks/:id/chat/messages | Список сообщений |
POST | /api/v1/tasks/:id/chat/messages | Отправить сообщение |
POST | /api/v1/tasks/:id/chat/files | Загрузить файл |
POST | /api/v1/tasks/:id/chat/messages/:message_id/save-file | Сохранить файл из чата в задачу/проект |
Сохранение файла из чата в задачу или проект
POST /api/v1/tasks/:id/chat/messages/:message_id/save-file
{
"file_id": 42,
"target": "task",
"folder_id": null
}target—"task"или"project".file_id— ID файла, прикреплённого к этому сообщению.folder_id— опциональная папка проекта (только дляtarget=project).
Метод создаёт новую запись файла с тем же S3-объектом, но с entity_type=task или entity_type=project. Исходный файл чата остаётся неизменным.
Ответ: 201 Created с объектом файла.
Редактирование и удаление сообщений
Редактировать и удалять может только автор или администратор.
| Метод | URL | Описание |
|---|---|---|
PUT | /api/v1/messages/:id | Изменить текст сообщения |
DELETE | /api/v1/messages/:id | Удалить сообщение (мягкое удаление) |
Тело запроса для редактирования: {"text": "новый текст"}.
После редактирования в объекте сообщения выставляется is_edited: true — используйте для отображения метки «изменено».
Удалённые сообщения скрываются из списка (deleted_at IS NOT NULL) и отправляют WS-событие message_deleted.
Пересылка сообщений
POST /api/v1/messages/:id/forward
{ "chat_ids": [1, 2, 3] }- Пересылает сообщение в несколько чатов за один вызов.
- Пользователь должен иметь доступ к исходному чату и быть участником каждого целевого чата.
chat_idsможет содержать любые доступные пользователю чаты:direct,group,project,task.- Новые сообщения получают
kind: "forwarded"и полеforwarded_fromс превью оригинала. - Вложенные файлы перелинковываются без повторной загрузки.
Ответ: 201 Created
{
"messages": [
{
"id": 501,
"chat_id": 2,
"user_id": 3,
"text": "Посмотрите на этот файл",
"kind": "forwarded",
"is_edited": false,
"forwarded_from_message_id": 388,
"forwarded_from": {
"id": 388,
"chat_id": 1,
"user_id": 7,
"user_name": "Иван Петров",
"text": "Посмотрите на этот файл",
"kind": "text",
"created_at": "2026-06-10T12:00:00Z"
},
"attachments": [...],
"created_at": "2026-06-11T15:00:00Z"
}
]
}После пересылки в каждый целевой чат отправляется WS-событие new_message.
Сообщения и упоминания
{
"text": "@Иван посмотри, пожалуйста",
"mention_user_ids": [2],
"file_ids": [15]
}Поле content поддерживается как alias для text для обратной совместимости.
Упоминание пользователя, который не видит чат (не участник и без доступа через задачу/проект), молча отбрасывается — сообщение всё равно отправляется. После отправки каждый упомянутый (кроме автора) получает уведомление chat.mention в /ws/notifications.
Реакции
Список доступных эмодзи: GET /api/v1/chats/reaction-options
Актуальный набор из 20 символов:
👍 👎 ❤️ 🔥 😂 😮 😢 😡 🙏 ✅
❌ 🎉 👀 💡 🚀 ⚡ 💯 🤔 👏 😍Реакция на сообщение
POST /api/v1/messages/:id/reactions
{ "type": "👍" }Реакция на конкретный файл внутри сообщения
Если в сообщении несколько файлов, реакцию можно поставить на конкретный вложенный файл:
POST /api/v1/messages/:id/reactions
{
"type": "🔥",
"target_type": "attachment",
"target_id": 42
}target_type—"message"(по умолчанию) или"attachment".target_id— ID вложения из поляattachments[].idу сообщения (0означает уровень сообщения).- Другой
target_type,target_idу реакции на сообщение или вложение из другого сообщения —400.
Один пользователь может поставить одну и ту же реакцию одновременно на сообщение и на каждый из файлов в нём — они хранятся независимо.
Реакции на файлы возвращаются в поле attachments[].reactions ответа на запрос сообщений; реакции на само сообщение — в reactions[].
Удалить реакцию:
DELETE /api/v1/messages/:id/reactions/👍
DELETE /api/v1/messages/:id/reactions/🔥?target_type=attachment&target_id=42Backend принимает только типы из разрешённого набора — произвольные строки вернут 400.
Файлы
Загрузка через multipart/form-data:
POST /api/v1/chats/:id/filesPOST /api/v1/tasks/:id/chat/files
Поля формы:
file— обязательный файл;category— произвольная категория (по умолчаниюgeneral);key— клиентский ключ для хранилища (необязательно, генерируется автоматически).
После загрузки ID файла передаётся в сообщение через file_ids. Максимальный размер — 1 ГБ: тело запроса больше лимита обрывается сервером (400 с текстом о лимите), клиент проверяет размер до отправки.
Вложения сообщения (attachments[]) содержат original_name, mime_type и size_bytes файла. Пересланное сообщение ссылается на тот же файл: его читает и скачивает любой, кому открыт хотя бы один чат, где файл приложен к неудалённому сообщению; так же работает сохранение файла из чата в задачу. Исполняемые файлы (.exe, .sh, .app и др.) заблокированы по расширению и MIME-типу; MIME определяется из содержимого файла, не из заголовка.
Пагинация сообщений
GET /api/v1/chats/:id/messages?before=388&limit=50
before— ID сообщения (включительно — курсор). Сообщения до него, в порядке убывания.limit— от 1 до 100 (по умолчанию 50).
Курсор составной (created_at, id) — сообщения с одинаковым timestamp не теряются.
WebSocket события
Подключение: GET /ws/chat.
Браузерный WebSocket() не умеет слать заголовок Authorization, поэтому подключение идёт по одноразовому тикету: access token в URL оседал бы в логах прокси и в истории.
POST /api/v1/ws/ticketс обычной аутентификацией →{"ticket": "…", "expires_at": "…"}. Тикет живёт минуту и гасится при первом использовании.ws://<host>/ws/chat?ticket=<ticket>.- На каждое переподключение запрашивается новый тикет.
Небраузерные клиенты используют Authorization: Bearer <token>. Старый способ ?token=<access_token> продолжает работать, но считается устаревшим.
| Событие | Когда |
|---|---|
new_message | Новое сообщение в чате |
message_updated | Сообщение изменено |
message_deleted | Сообщение удалено |
reaction_added | Добавлена реакция |
reaction_removed | Удалена реакция |
ping | Пользователь пинганул другого |
participant_removed | Пользователь удалён из чата |
chat_read | Пользователь прочитал чат до message_id |
participant_added | Пользователя добавили в чат (новый личный/групповой чат, участник задачи или команды проекта). Поле chat — сам чат; сервер уже подписал открытые соединения пользователя на chat_id |
Структура события:
{
"type": "new_message",
"chat_id": 12,
"message_id": 388,
"message": { ... },
"reader_id": null,
"timestamp": "2026-06-11T14:22:00Z"
}Для chat_read: reader_id — кто прочитал, message_id — до какого сообщения.
Живость соединения: сервер шлёт ping-кадр каждые 30 секунд (браузер отвечает pong сам), клиент каждые 30 секунд шлёт {"type": "heartbeat"}. Без кадров от клиента дольше 70 секунд сервер закрывает соединение. То же для /ws/notifications.
Групповой чат проекта
Чат проекта создаётся автоматически при первом добавлении участника в команду проекта (включая приглашённых, даже не подтвердивших участие). При старте сервера все существующие неархивные проекты с активной командой, у которых нет чата, дополучают его автоматически (backfill).
Управляется переменной окружения APP_AUTO_CREATE_PROJECT_CHAT (по умолчанию true, отключается явным false).
При добавлении / удалении участника из команды проекта через POST /api/v1/projects/:id/team или DELETE /api/v1/projects/:id/team/:user_id участник автоматически добавляется / удаляется из проектного чата.
Создание чатов
POST /api/v1/chats
{
"type": "direct",
"participant_ids": [5]
}POST /api/v1/chats
{
"type": "group",
"title": "Мозговой штурм",
"participant_ids": [2, 3, 4]
}Для direct — один получатель в participant_ids. Если чат уже существует — возвращается существующий.
Публичное создание принимает только direct и group. Чаты project и task создаются автоматически бизнес-логикой.
Управление участниками
Добавить: POST /api/v1/chats/:id/participants — {"user_ids": [6, 7]}
Удалить: DELETE /api/v1/chats/:id/participants/:user_id
Удалённый участник получает событие participant_removed и теряет WS-подписку немедленно.
Инструкция для frontend
Глобальный список чатов:
GET /api/v1/chats— все чаты пользователя сlast_messageиunread_count, плюсdirect_contactsсо всеми активными сотрудниками для старта личного чата.- Сортировка уже по дате последнего сообщения.
- При открытии чата и прочтении последнего сообщения вызвать
POST /api/v1/chats/:id/readс{"message_id": last_message.id}. unread_count > 0→ показывать бейдж.
Чат задачи:
- При открытии карточки задачи —
GET /api/v1/tasks/:id/chat. - Сообщения —
GET /api/v1/tasks/:id/chat/messages?limit=50. - Контекстное меню сообщения:
- Реакция на сообщение:
POST /api/v1/messages/:id/reactionsс{"type": "👍"}. - Реакция на конкретный файл: то же самое с
{"type": "🔥", "target_type": "attachment", "target_id": <attachments[i].id>}. - Реакции на файлы отображаются в
attachments[i].reactions, на само сообщение — вreactions. - «Редактировать»:
PUT /api/v1/messages/:idс{"text": "..."}. Показывайте метку «изменено» еслиis_edited: true. - «Удалить»:
DELETE /api/v1/messages/:id. По WS-событиюmessage_deletedскройте сообщение. - «Переслать»:
POST /api/v1/messages/:id/forwardс{"chat_ids": [...]}. Покажите пикер чатов из глобального списка. - «Добавить в файлы задачи»:
POST /api/v1/tasks/:id/chat/messages/:msg_id/save-fileс{"file_id": N, "target": "task"}. - «Добавить в файлы проекта»: то же самое с
"target": "project".
- Реакция на сообщение:
- Отображение пересланного сообщения:
kind === "forwarded"→ показать блок сforwarded_from.user_nameи цитатой текста над основным телом. - Эти действия доступны только если у сообщения есть вложения (
attachmentsнепустой).
Emoji-панель:
- Кэшировать результат
GET /api/v1/chats/reaction-options— он не меняется без деплоя. - Отображать только эмодзи из этого списка — backend блокирует всё остальное.