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

POST /api/messenger/chats/{chatId}/messages/attachment — Отправить вложение

Назначение

Участник отправляет сообщение-вложение (Kind = Attachment). Бинарь файла в messenger не хранится (INV-MSG-7): клиент предварительно загружает файл в storage/CDN по пресайн-URL (вне этой операции) и присылает только FileId + метаданные (FileName/ContentType/SizeBytes + опциональная Caption). Как и для текста: атомарно выдаётся per-chat Seq, захватываются IpAddress/UserAgent (INV-MSG-6), публикуется MessagePostedEvent и уходит realtime-push. Дом полей — message.md.

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areamessages
Feature groupmessages
Статусdraft
Документdocs/06-services/messenger/api/messages/send-attachment-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/FileId/FileName в логи не пишутся

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ChatIdrouteGuidДа!= Guid.Empty; чат существует; actor — активный участник; Status != Closed; политика (см. Алгоритм)
FileIdbodyGuidДа!= Guid.Empty; логическая ссылка на storage/CDN (бинарь не в БД, INV-MSG-7)Internal: в логи не пишется
FileNamebodystringДа1..256, не пустое после trimможет нести детали — не логируется
ContentTypebodystringДа1..128; допустимый MIME (конфигурация) → AttachmentKind (см. Алгоритм)
SizeBytesbodylongДа> 0 и <= <limit> (конфигурация)защита от unbounded
Captionbodystring?Нет<= 2000 после trimPII: подпись в логи не пишется
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); для Attachment непуста под-модель Attachment. DownloadUrl — короткоживущий presign у storage-адаптера (не логируется). IP/UA/история правок не входят.

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаMessageEntity.IdИдентификатор сообщения.
SeqlongДаper-chat порядковыйВыданный порядковый.
Kindstring (MessageKind)ДаKindВсегда Attachment.
SenderUserIdGuidДаSenderUserIdАвтор (= actor).
SenderRolestring (ParticipantRole)ДаSenderRoleРоль автора на момент отправки.
SentAtDateTimeOffsetДаSentAtМомент отправки.
EditableUntilDateTimeOffsetДаSentAt + 60sОкно правки подписи/удаления (INV-MSG-1).
AttachmentMessageAttachmentModelДаAttachmentMessageEntity{ FileId, FileName, ContentType, SizeBytes, Caption?, AttachmentKind, DownloadUrl }.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь-участник; требуется право messenger.chats.write (иначе — FORBIDDEN; без JWT — UNAUTHORIZED). ChatId берётся из маршрута; метаданные вложения — из тела; ClientMessageId — из Idempotency-Key; IpAddress/UserAgent захватываются сервером. По ClientMessageId проверяется идемпотентность: если запрос уже обработан — вернуть сохранённое сообщение.
  2. Проверка входа и предусловий. FileId непуст, FileName 1..256, ContentType 1..128 из белого списка, SizeBytes > 0 && <= limit, Caption <= 2000, ClientMessageId непуст, IP/UA заданы (INV-MSG-6/7) — иначе VALIDATION_FAILED/MSG_ATTACHMENT_INVALID. Актор должен быть активным участником — иначе MSG_CHAT_ACCESS_DENIED. Чат не Closed — иначе MSG_CHAT_CLOSED. Коммуникационная политика для Direct-чата: ни одна сторона не в чёрном списке (INV-CHAT-3) — иначе MSG_COMMUNICATION_BLOCKED.
  3. Что читаем. Чат — для монотонной выдачи Seq (сериализация конкурентных отправок, INV-CHAT-5/ INV-MSG-3) и источника роли участника. Наличие файла в storage синхронно не проверяется (граница storage, INV-MSG-7); валидность presign — при скачивании.
  4. Бизнес-правила и маппинг. AttachmentKind определяется из ContentType (Image/Document/Audio/ Video/Other). Создаётся сообщение-вложение: SenderUserId = actor, SenderRole — роль участника, IsSystem = false, метаданные файла, SentAt = now, EditableUntil = now + 60s, IpAddress/UserAgent. Чат выдаёт Seq (per-chat монотонный) и обновляет денормализацию последнего сообщения; LastMessagePreview = FileName/иконка типа (без тела вложения).
  5. Что меняется. Появляется сообщение, чат обновляет денормализацию и счётчик Seq. Всё фиксируется в одном согласованном изменении с публикацией события, намерением realtime-доставки и отметкой идемпотентности; конфликт выдачи SeqMSG_SEQ_CONFLICT (авто-retry).
  6. Факты и события. Публикуется интеграционное событие MessagePostedEvent в топик pin.messenger.message-posted.v1 (ContractVersion = 1, ключ ChatId, payload {ChatId, MessageId, Seq, Kind = Attachment, SenderUserId, SenderRole, SentAt, Preview = FileName/тип}без IP/UA/бинаря; консюмер notifications, контракт message-posted.md). Плюс намерение realtime-доставки для realtime-dispatch (persist-then-push → хаб MessageReceived, группа chat:{ChatId}).
  7. Результат. MessageModel (Kind = Attachment, Seq, Attachment с presign DownloadUrl), 201.

Диаграмма

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

ТипДетали
Интеграционные событияMessagePostedEventpin.messenger.message-posted.v1 (key ChatId, консюмер notifications)
Внешние вызовыstorage/CDN presign download-URL при маппинге ответа (бинарь не грузится сервисом; TTL короткий)
Проекции / поискденорм last-message на ChatEntity (LastMessageId/Seq/At/Preview)
Уведомленияin-app realtime MessageReceived; offline push/email — notifications как консюмер события

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400пустые ChatId/ClientMessageId, нет IP/UA«Проверьте параметры вложения»no retry
MSG_ATTACHMENT_INVALID400пустой FileId, недопустимый ContentType, SizeBytes вне пределов, пустой FileName (INV-MSG-7)«Недопустимое вложение (тип/размер)»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, валидные метаданные (FileId, FileName 1..256, ContentType из белого списка, SizeBytes в пределах). Когда POST. Тогда создаётся сообщение Kind = Attachment, выдаётся per-chat Seq, AttachmentKind выведен из ContentType, публикуется MessagePostedEvent в pin.messenger.message-posted.v1 (ключ ChatId, без бинаря/IP/UA), push MessageReceived, ответ 201 c presign DownloadUrl.
  • Идемпотентный повтор. Дано тот же Idempotency-Key (ClientMessageId). Когда retry/reconnect. Тогда возвращается ранее сохранённое сообщение, дубля нет.
  • Недопустимое вложение. Дано ContentType вне белого списка, SizeBytes вне пределов, пустой FileId или FileName. Когда POST. Тогда MSG_ATTACHMENT_INVALID (400) (INV-MSG-7).
  • Чёрный список (Direct). Дано Direct-чат и одна из сторон в чёрном списке. Когда POST. Тогда MSG_COMMUNICATION_BLOCKED (409) (INV-CHAT-3).
  • Закрытый чат. Дано чат Closed. Когда POST. Тогда MSG_CHAT_CLOSED (409) (INV-CHAT-8).
  • Не участник. Дано актор не активный участник. Когда POST. Тогда MSG_CHAT_ACCESS_DENIED (403).
  • Гонка выдачи Seq. Дано конкурентные отправки в один чат. Когда Seq конфликтует. Тогда MSG_SEQ_CONFLICT (409) с авто-retry; итоговые Seq монотонны и уникальны (INV-MSG-3).
  • Бинарь вне сервиса. Дано messenger не хранит файл (INV-MSG-7). Когда отправка. Тогда наличие файла в storage синхронно не проверяется; валидность presign проверяется при скачивании.
  • Приватность. FileId/FileName/Caption/IP/UA не логируются и не входят в событие.

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

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

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

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

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