Skip to content

Chat System ​

Назначение ​

Единый глобальный список чатов объединяет все типы коммуникаций: личные переписки, групповые чаты, чаты проектов и чаты задач. Все они доступны через GET /api/v1/chats и реал-тайм через WebSocket /ws/chat.

Чат задачи одновременно встроен в карточку задачи и виден в глобальном списке — пользователь не теряет сообщения независимо от того, откуда он открыл чат.

Типы чатов ​

ТипСоздаётсяУчастники
directPOST /api/v1/chats, POST /api/v1/users/:id/pingДва пользователя
groupPOST /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].

Каждый элемент списка содержит:

json
{
  "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 — пометить чат как прочитанный до указанного сообщения.

json
{ "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

json
{
  "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

json
{ "chat_ids": [1, 2, 3] }
  • Пересылает сообщение в несколько чатов за один вызов.
  • Пользователь должен иметь доступ к исходному чату и быть участником каждого целевого чата.
  • chat_ids может содержать любые доступные пользователю чаты: direct, group, project, task.
  • Новые сообщения получают kind: "forwarded" и поле forwarded_from с превью оригинала.
  • Вложенные файлы перелинковываются без повторной загрузки.

Ответ: 201 Created

json
{
  "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.

Сообщения и упоминания ​

json
{
  "text": "@Иван посмотри, пожалуйста",
  "mention_user_ids": [2],
  "file_ids": [15]
}

Поле content поддерживается как alias для text для обратной совместимости.

Упоминание пользователя, который не видит чат (не участник и без доступа через задачу/проект), молча отбрасывается — сообщение всё равно отправляется. После отправки каждый упомянутый (кроме автора) получает уведомление chat.mention в /ws/notifications.

Реакции ​

Список доступных эмодзи: GET /api/v1/chats/reaction-options

Актуальный набор из 20 символов:

👍 👎 ❤️ 🔥 😂 😮 😢 😡 🙏 ✅
❌ 🎉 👀 💡 🚀 ⚡ 💯 🤔 👏 😍

Реакция на сообщение ​

json
POST /api/v1/messages/:id/reactions
{ "type": "👍" }

Реакция на конкретный файл внутри сообщения ​

Если в сообщении несколько файлов, реакцию можно поставить на конкретный вложенный файл:

json
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=42

Backend принимает только типы из разрешённого набора — произвольные строки вернут 400.

Файлы ​

Загрузка через multipart/form-data:

  • POST /api/v1/chats/:id/files
  • POST /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 оседал бы в логах прокси и в истории.

  1. POST /api/v1/ws/ticket с обычной аутентификацией → {"ticket": "…", "expires_at": "…"}. Тикет живёт минуту и гасится при первом использовании.
  2. ws://<host>/ws/chat?ticket=<ticket>.
  3. На каждое переподключение запрашивается новый тикет.

Небраузерные клиенты используют 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

Структура события:

json
{
  "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 участник автоматически добавляется / удаляется из проектного чата.

Создание чатов ​

json
POST /api/v1/chats
{
  "type": "direct",
  "participant_ids": [5]
}
json
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 ​

Глобальный список чатов:

  1. GET /api/v1/chats — все чаты пользователя с last_message и unread_count, плюс direct_contacts со всеми активными сотрудниками для старта личного чата.
  2. Сортировка уже по дате последнего сообщения.
  3. При открытии чата и прочтении последнего сообщения вызвать POST /api/v1/chats/:id/read с {"message_id": last_message.id}.
  4. unread_count > 0 → показывать бейдж.

Чат задачи:

  1. При открытии карточки задачи — GET /api/v1/tasks/:id/chat.
  2. Сообщения — GET /api/v1/tasks/:id/chat/messages?limit=50.
  3. Контекстное меню сообщения:
    • Реакция на сообщение: 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".
  4. Отображение пересланного сообщения: kind === "forwarded" → показать блок с forwarded_from.user_name и цитатой текста над основным телом.
  5. Эти действия доступны только если у сообщения есть вложения (attachments непустой).

Emoji-панель:

  1. Кэшировать результат GET /api/v1/chats/reaction-options — он не меняется без деплоя.
  2. Отображать только эмодзи из этого списка — backend блокирует всё остальное.

Загружаем документацию…