Перейти к основному содержимому

POST /api/messenger/chats/{chatId}/messages/text — Отправить текстовое сообщение

Назначение

Участник отправляет текстовое сообщение (Kind = Text) в чат. Операция создаёт сообщение, атомарно выдаёт per-chat Seq (INV-CHAT-5), захватывает IpAddress/UserAgent отправителя (INV-MSG-6), публикует MessagePostedEvent (консюмер notifications) и через persist-then-push доставляет MessageReceived в хаб. Дом полей — message.md.

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areamessages
Feature groupmessages
Статусdraft
Документdocs/06-services/messenger/api/messages/send-text-message.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Инвестор / Агент / Руководитель / Менеджер застройщика / Менеджер ПИН)
Auth policy / Permissionsauthenticated (JWT) + permission messenger.chats.write
Scope (RBAC)не применяется: доступ по членству (ADR-0055/0052)
Record-levelактивный ParticipantEntity (ChatId,UserId = actor, RemovedAt is null) — INV-CHAT-4
Tenant isolationне tenanted: чат кросс-сторонний; изоляция — членство (chat.md)
Auditcreate (MessageEntity); SenderUserId = actor; IpAddress/UserAgent — Sensitive/internal, в логи/события не пишутся

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/chats/{chatId}/messages/text
Success status201 (сообщение создано)
Idempotency headerIdempotency-Key (= ClientMessageId, Guid) — дедуп двойной отправки при retry/reconnect
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ChatIdrouteGuidДа!= Guid.Empty; чат существует; actor — активный участник; Status != Closed (INV-CHAT-8); политика (см. Алгоритм)
BodybodystringДа1..8000 после trim (не пустое); контроль-символы нормализуютсяPII: тело в логи не пишется
ClientMessageIdheader Idempotency-KeyGuidДа!= Guid.Empty; ключ идемпотентностидедуп ретраев
UserIdidentity claimsGuidДаRequired; SenderUserId, предикат членстваscope isolation
IpAddressserver (remote/XFF)IpAddressДазахват сервером (INV-MSG-6); не из телаSensitive/internal
UserAgentheader User-AgentstringДа<= 512; захват сервером (INV-MSG-6)Sensitive/internal

Модель ответа MessageModel

Проекция созданного сообщения (та же union-модель, что в list-messages.md); для Text непуста под-модель Text = { Body }. PII-Body возвращается только автору-инициатору в ответе; IP/UA/ история правок в ответ не входят.

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаMessageEntity.IdИдентификатор сообщения.
SeqlongДаper-chat порядковыйВыданный порядковый.
Kindstring (MessageKind)ДаKindВсегда Text.
SenderUserIdGuidДаSenderUserIdАвтор (= actor).
SenderRolestring (ParticipantRole)ДаSenderRoleРоль автора на момент отправки.
SentAtDateTimeOffsetДаSentAtМомент отправки.
EditableUntilDateTimeOffsetДаSentAt + 60sГраница окна правки/удаления (INV-MSG-1).
TextMessageTextModelДаTextMessageEntity{ Body: string }.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь-участник; требуется право messenger.chats.write (иначе — FORBIDDEN; без JWT — UNAUTHORIZED). ChatId берётся из маршрута, Body — из тела, ClientMessageId — из Idempotency-Key; IpAddress/UserAgent захватываются сервером. По ClientMessageId проверяется идемпотентность: если запрос уже обработан — вернуть сохранённое сообщение (201), не создавая дубль.
  2. Проверка входа и предусловий. Body 1..8000 (trim, не пустое), ChatId/ClientMessageId непусты, UserAgent <= 512, IpAddress задан (INV-MSG-6) — иначе VALIDATION_FAILED. Актор должен быть активным участником — иначе MSG_CHAT_ACCESS_DENIED. Чат не Closed — иначе MSG_CHAT_CLOSED. Коммуникационная политика для Direct-чата: ни одна сторона не в чёрном списке (blacklist.md, contact.md, INV-CHAT-3) — иначе MSG_COMMUNICATION_BLOCKED.
  3. Что читаем. Чат — для монотонной выдачи Seq (сериализация конкурентных отправок, INV-CHAT-5/ INV-MSG-3) и источника роли участника.
  4. Бизнес-правила и маппинг. Создаётся текстовое сообщение: SenderUserId = actor, SenderRole — роль участника, IsSystem = false, Body, SentAt = now, EditableUntil = now + 60s, IpAddress/ UserAgent. Чат выдаёт Seq (per-chat монотонный), обновляет LastMessageId/Seq/At и LastMessagePreview (ограничено ≤ 256).
  5. Что меняется. Появляется сообщение, чат обновляет денормализацию и счётчик Seq. Всё фиксируется в одном согласованном изменении с публикацией события, намерением realtime-доставки и отметкой идемпотентности; конфликт выдачи SeqMSG_SEQ_CONFLICT (авто-retry).
  6. Факты и события. Публикуется интеграционное событие MessagePostedEvent в топик pin.messenger.message-posted.v1 (ContractVersion = 1, ключ партиционирования ChatId, payload {ChatId, MessageId, Seq, Kind = Text, SenderUserId, SenderRole, SentAt, Preview}без IP/UA/тела; консюмер notifications, контракт message-posted.md). Плюс намерение realtime-доставки для realtime-dispatch (persist-then-push → хаб MessageReceived, группа chat:{ChatId}).
  7. Результат. MessageModel (Kind = Text, Seq, EditableUntil, Text), 201.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияMessagePostedEventpin.messenger.message-posted.v1 (key ChatId, консюмер notifications)
Проекции / поискденорм last-message на ChatEntity (LastMessageId/Seq/At/Preview)
Уведомленияin-app realtime MessageReceived (persist-then-push); offline push/email — notifications как консюмер события

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Body пуст/> 8000, пустой ChatId/ClientMessageId, нет IP/UA (INV-MSG-6)«Проверьте текст сообщения»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
FORBIDDEN403нет permission messenger.chats.write«Недостаточно прав»no retry
MSG_CHAT_ACCESS_DENIED403actor — не активный участник (или чат не существует)«Нет доступа к чату»no retry
MSG_COMMUNICATION_BLOCKED409чёрный список любой из сторон (INV-CHAT-3)«Отправка сообщений недоступна»no retry
MSG_CHAT_CLOSED409чат Closed (сделка завершена/отменена, INV-CHAT-8)«Чат закрыт»no retry
MSG_SEQ_CONFLICT409гонка выдачи Seq (INV-MSG-3)«Повторите отправку»retry (авто)

Acceptance Criteria

  • Отправка текста. Дано активный участник, чат не Closed, Body в 1..8000 после trim, ни одна сторона не в чёрном списке. Когда POST. Тогда создаётся сообщение Kind = Text, выдаётся per-chat Seq (INV-CHAT-5), публикуется MessagePostedEvent в pin.messenger.message-posted.v1 (ключ ChatId, без тела/IP/UA), push MessageReceived (persist-then-push), ответ 201 с EditableUntil = SentAt + 60s.
  • Идемпотентный повтор. Дано тот же Idempotency-Key (ClientMessageId). Когда retry/reconnect. Тогда возвращается ранее сохранённое сообщение (201), дубля нет.
  • Пустое/длинное тело. Дано Body пуст после trim или > 8000. Когда POST. Тогда VALIDATION_FAILED (400).
  • Чёрный список (Direct). Дано Direct-чат и одна из сторон в чёрном списке (INV-CHAT-3). Когда POST. Тогда MSG_COMMUNICATION_BLOCKED (409).
  • Закрытый чат. Дано чат Closed (сделка завершена/отменена, INV-CHAT-8). Когда POST. Тогда MSG_CHAT_CLOSED (409).
  • Не участник. Дано актор не активный участник. Когда POST. Тогда MSG_CHAT_ACCESS_DENIED (403).
  • Гонка выдачи Seq. Дано конкурентные отправки в один чат. Когда Seq конфликтует. Тогда MSG_SEQ_CONFLICT (409) с авто-retry; Seq остаётся монотонным per-chat (INV-MSG-3).
  • Приватность. Body/IP/UA не входят в событие и логи (INV-MSG-6); PII-Body возвращается только автору-инициатору в ответе.

Совместимость и наблюдаемость

  • Новые optional поля модели/payload — additive; удаление/переименование/смена типа или значения enum — breaking (migration + rollout; событие — новый .v2 + dual-publish). Лимит Body — конфигурация (расширение additive).
  • Logs: text-message-posted (ChatId, MessageId, Seq, SenderUserId; Body/IP/UA не пишутся); metrics: messenger_messages_posted_total{kind,result}, persist p95 < 300 мс (SLO); traces через traceparent; dashboard pin-messenger.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы