POST /api/messenger/chats/direct — Открыть/создать личный 1:1 чат
Назначение
Открыть личный 1:1-чат (ChatEntity, Kind = Direct) между актором и указанным
пользователем. Идемпотентно по неупорядоченной паре (DirectKey, INV-CHAT-1): повторный вызов не плодит чат,
а возвращает существующий. Проверяется коммуникационная политика (чёрный список любой стороны, INV-CHAT-3 /
INV-BL-3): открыть чат с заблокировавшим/заблокированным нельзя.
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | chats |
| Feature group | chats |
| Статус | draft |
| Документ | docs/06-services/messenger/api/chats/open-direct-chat.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (инициатор — любая сторона) |
| Auth policy / Permissions | permission messenger.chats.write — каталог ../../../identity/domain/permission.md |
| Scope (RBAC) | не применяется — общение кросс-стороннее, вне scope/tenant (ADR-0052 п.3) |
| Record-level | инициатор становится участником создаваемого/существующего чата пары; чужие чаты недоступны |
| Tenant isolation | нет (кросс-стороннее общение; messenger не tenanted строково) |
| Audit | create (при создании — CreatedByUserId = actor); targetUserId — id (не PII), ФИО не логируется |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/messenger/chats/direct |
| Success status | 200 (чат уже существовал) / 201 (создан) — различает поле Created |
| Idempotency header | не применяется — идемпотентность естественная по DirectKey (INV-CHAT-1), не по заголовку |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | identity claims | Guid | Да | Required; permission messenger.chats.write | инициатор пары |
TargetUserId | body | Guid | Да | != Guid.Empty; != ActorUserId (INV-CHAT-7); существующий IsActive пользователь (шаг 2) | — |
Тело — типизированная модель OpenDirectChatRequest { TargetUserId: Guid }.
Модель ответа OpenDirectChatResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
ChatId | Guid | Да | Chat.Id | — | Идентификатор личного чата пары. |
Created | bool | Да | computed (шаг 4) | — | true — чат создан этим вызовом; false — уже существовал (идемпотентно). |
Kind | ChatKind (enum→string) | Да | Chat.Kind | — | Всегда Direct. |
Status | ChatStatus (enum→string) | Да | Chat.Status | — | Как правило Active. |
CounterpartyUserId | Guid | Да | TargetUserId | — | Собеседник (для навигации/подписи). |
CreatedAt | DateTimeOffset | Да | Chat.CreatedAt | — | Момент создания (существовавшего или нового). |
Предусловия и постусловия
Предусловия. Актор авторизован и имеет messenger.chats.write. Адресат (TargetUserId) существует в реплике
пользователей и активен (IsActive, INV-CT-6); TargetUserId != ActorUserId (INV-CHAT-7). Ни одна сторона не в
чёрном списке другой (INV-CHAT-3 / INV-BL-3).
Постусловия. Существует ровно один Direct-чат неупорядоченной пары (DirectKey, INV-CHAT-1) со статусом
Active и двумя активными участниками (Role = Member, LastReadSeq = 0). При Created = true дополнительно:
в ленту чата добавлена строка ChatCreated, обеим сторонам отправлен realtime ChatCreated. При Created = false
существующий чат не мутирован. Kafka-события не публикуются (Direct-чат кросс-сервисных потребителей не имеет).
Алгоритм
- Контекст и доступ. Операцию выполняет инициатор (
User, любая сторона); ActorUserId — из claims;
требуется право messenger.chats.write (иначе MESSENGER_FORBIDDEN). Идемпотентность естественная по
DirectKey (INV-CHAT-1), не по заголовку.
- Проверка входа.
TargetUserId непустой и != ActorUserId (INV-CHAT-7); иначе VALIDATION_FAILED
либо MESSENGER_DIRECT_SELF. Адресат существует и активен (реплика, INV-CT-6); иначе
MESSENGER_USER_NOT_FOUND.
- Ключ пары и политика. Вычислить
DirectKey из неупорядоченной пары (min:max по Guid) —
уникальность ux_chats_direct_key. Найти существующий Direct-чат пары. Проверить коммуникационную
политику CommunicationPolicy.CanMessage — чёрный список любой стороны (INV-BL-3).
- Создание или идемпотентный возврат. Если общение заблокировано →
MESSENGER_COMMUNICATION_BLOCKED
(INV-CHAT-3, без раскрытия инициатора блокировки). Если чат пары уже есть → идемпотентный возврат
(Created = false), чат не мутируется. Иначе создать чат (Kind = Direct, Status = Active,
DirectKey, служебный Title-placeholder — ФИО резолвится на чтении) с двумя участниками
(Role = Member, LastReadSeq = 0, RemovedAt = null, INV-CHAT-7/INV-PART-3); Created = true.
- Лента и реалтайм. При создании — в той же согласованной операции появляется строка ленты
ChatEventEntity типа ChatCreated (chat-event.md) и persist-then-push
ChatCreated в SignalR-hub обеим сторонам (ADR-0055). Интеграционных Kafka-событий нет (Direct-чат не
имеет кросс-сервисных потребителей; pin.messenger.deal-chat-status-changed.v1 — только для Deal-чатов).
- Согласованность. При создании чат, его участники и строка ленты фиксируются атомарно; гонку
параллельного открытия той же пары ловит уникальный
ux_chats_direct_key → перечитать и вернуть
существующий (Created = false, идемпотентность INV-CHAT-1).
- Результат.
OpenDirectChatResult (ChatId, Created, Kind, Status, CounterpartyUserId,
CreatedAt) → HTTP 201 Created при создании, 200 OK при возврате существующего.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none (Direct-чат Kafka-событий не публикует; см. README produced-события) |
| External calls | none (адресат — реплика messenger-user.md; блок-лист — своя схема) |
| Cache / projections / search | none |
| Notifications | realtime: persist-then-push ChatCreated в SignalR-hub обеим сторонам (ADR-0055); offline-push — нет |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | TargetUserId = Guid.Empty | «Некорректный получатель» | no retry |
MESSENGER_DIRECT_SELF | 400 | TargetUserId = ActorUserId (INV-CHAT-7) | «Нельзя открыть чат с самим собой» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | re-login |
MESSENGER_FORBIDDEN | 403 | нет permission messenger.chats.write | «Недостаточно прав» | no retry |
MESSENGER_USER_NOT_FOUND | 404 | адресата нет в реплике или IsActive = false (INV-CT-6) | «Пользователь не найден» | no retry |
MESSENGER_COMMUNICATION_BLOCKED | 409 | чёрный список любой стороны (INV-CHAT-3 / INV-BL-3) | «Общение с пользователем недоступно» | no retry |
Acceptance Criteria
- Первое открытие (happy). Дано: между актором и адресатом нет
Direct-чата, стороны не в чёрных списках.
Когда актор открывает чат. Тогда создаётся один Direct-чат (Status = Active, два участника), Created = true,
201; обе стороны получают realtime ChatCreated, в ленте — строка ChatCreated.
- Идемпотентный повтор. Дано: чат пары уже существует. Когда любая сторона открывает чат по тому же адресату.
Тогда возвращается тот же
ChatId, Created = false, 200; новый чат не создаётся, лента не растёт.
- Симметрия пары. Дано: A ранее открыл чат с B. Когда B открывает чат с A. Тогда возвращается тот же чат
(
DirectKey симметричен, INV-CHAT-1), Created = false.
- Заблокированное общение. Дано: одна из сторон в чёрном списке другой. Когда актор открывает чат. Тогда
409 MESSENGER_COMMUNICATION_BLOCKED, чат не создаётся (инициатор блокировки не раскрывается).
- Чат с собой. Дано:
TargetUserId == ActorUserId. Когда актор открывает чат. Тогда 400 MESSENGER_DIRECT_SELF.
- Неизвестный/неактивный адресат. Дано: адресата нет в реплике или
IsActive = false. Когда актор открывает
чат. Тогда 404 MESSENGER_USER_NOT_FOUND.
- Гонка параллельного открытия. Дано: две параллельные попытки открыть чат той же пары. Тогда создаётся ровно
один чат (
ux_chats_direct_key), проигравшая попытка возвращает существующий (Created = false), дубля нет.
Совместимость и наблюдаемость
- Новые optional-поля ответа — additive; смена типа
TargetUserId/ChatId или значения enum
ChatKind/ChatStatus — breaking (migration + rollout). Идемпотентный контракт (Created) стабилен.
- Logs:
direct-chat opened (ChatId, Created, ActorUserId, TargetUserId — id; ФИО не логируется);
metrics: messenger_open_direct_total{created,result}, latency p95 (< 300 мс, README);
traces — сквозной trace (traceparent); dashboard pin-messenger.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы
- Сущности: chat.md (INV-CHAT-1/3/7,
DirectKey, фабрика OpenDirect),
participant.md (INV-PART-3), blacklist.md
(CommunicationPolicy, INV-BL-3), messenger-user.md (адресат, IsActive),
chat-event.md (лента ChatCreated).
- Смежные: get-chat.md, list-chats.md; карта областей — README.
- Permission
messenger.chats.write: ../../../identity/domain/permission.md.
- ADR: 0055 мини-мессенджер,
0052 tenancy.