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

DELETE /api/messenger/chats/{chatId}/messages/{messageId} — Удаление сообщения (мягкое, ≤ 1 мин)

Назначение

Автор удаляет своё сообщение в окне EditableUntil (≤ 60 с, INV-MSG-1). Удаление мягкое: строка остаётся ради монотонного Seq и порядка ленты, но тело/подпись скрываются — клиент показывает «сообщение удалено». Перед скрытием пишется append-only снимок прошлого контента MessageEditEntity (для admin-арбитража, читателям не показывается — INV-MSG-2). Карточки (EventCard) и системные сообщения удалять нельзя (INV-MSG-4). Участникам чата уходит realtime-push MessageDeleted. Дом полей — message.md.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (автор сообщения)
Auth policy / Permissionsauthenticated (JWT) + permission messenger.messages.edit
Scope (RBAC)не применяется: доступ по членству + авторству (ADR-0055/0052)
Record-levelавтор: message.SenderUserId = actor.UserId (INV-MSG-1) + активный Participant
Tenant isolationне tenanted: изоляция — членство + авторство (chat.md)
Auditsoft-delete (MessageEntity.DeletedAt) + append MessageEditEntity (снимок прошлого, EditorUserId = actor); IP/UA — Sensitive/internal

HTTP-контракт

ПолеЗначение
MethodDELETE
Route/api/messenger/chats/{chatId}/messages/{messageId}
Success status200 (tombstone-проекция; повтор на уже удалённом в окне — тоже 200, идемпотентно)
Idempotency headerне применяется (мягкое удаление естественно идемпотентно; конкуренция — оптимистическая блокировка)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ChatIdrouteGuidДа!= Guid.Empty; чат существует; actor — активный участник
MessageIdrouteGuidДа!= Guid.Empty; сообщение существует, ChatId совпадает; SenderUserId = actor; now <= EditableUntil; Kind != EventCard (см. Алгоритм)
UserIdidentity claimsGuidДаRequired; предикат членства + авторстваscope isolation
IpAddressserver (remote/XFF)IpAddressДазахват сервером; снимок удаленияSensitive/internal
UserAgentheader User-AgentstringДа<= 512; захват серверомSensitive/internal

Тело запроса пустое: удаление адресуется только по route-идентификаторам.

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

Union-проекция удалённого сообщения (как в list-messages.md): IsDeleted = true, тело/ подпись не отдаются (под-модели Text/Attachment = null), Seq сохранён для порядка ленты. История правок и IP/UA не входят (INV-MSG-2).

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаMessageEntity.IdИдентификатор сообщения.
SeqlongДаSeqПорядок в ленте (сохраняется — строка не физически удалена).
Kindstring (MessageKind)ДаKindText / Attachment.
IsDeletedboolДаDeletedAt != nullВсегда true; клиент рендерит «сообщение удалено».
DeletedAtDateTimeOffsetДаDeletedAtМомент удаления.
TextMessageTextModel?НетскрытоВсегда null (тело скрыто).
AttachmentMessageAttachmentModel?НетскрытоВсегда null (метаданные/FileId скрыты).

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь-автор; требуется право messenger.messages.edit (иначе — FORBIDDEN; без JWT — UNAUTHORIZED). ChatId/MessageId берутся из маршрута; IpAddress/ UserAgent захватываются сервером.
  2. Проверка входа и предусловий. ChatId/MessageId непусты — иначе VALIDATION_FAILED. Актор должен быть активным участником чата — иначе MSG_CHAT_ACCESS_DENIED. Сообщение должно существовать и принадлежать этому чату — иначе MSG_MESSAGE_NOT_FOUND. Автор, тип и окно (INV-MSG-1/4): SenderUserId == actor иначе MSG_EDIT_FORBIDDEN; сообщение не EventCard и не системное иначе MSG_EDIT_FORBIDDEN; now <= EditableUntil иначе MSG_EDIT_WINDOW_EXPIRED. Если сообщение уже удалено — вернуть текущий tombstone (200, без изменений).
  3. Что читаем. Сообщение (для проверок и снимка); при удалении последнего сообщения чата дополнительно сам чат — для денормализации LastMessagePreview.
  4. Бизнес-правила и переход. Рассчитать Revision = (max prior)+1; записать снимок прошлого контента в MessageEditEntity (PreviousBody/PreviousCaption, EditorUserId = actor, EditedAt = now, IpAddress/ UserAgent); проставить DeletedAt = now (тело/подпись скрываются на чтении). Переход состояния Sent/Edited → Deleted (INV-MSG-1). Если удаляется последнее сообщение чата — LastMessagePreview = "Сообщение удалено" (LastMessageSeq/Seq не меняются).
  5. Что меняется. Обновляется сообщение (мягкое удаление) + добавляется снимок MessageEditEntity (+ обновляется чат при денормализации). Всё фиксируется в одном согласованном изменении (INV-MSG-2); оптимистический конфликт → MSG_EDIT_CONFLICT (перечитать).
  6. Факты и события. Интеграционного Kafka-события нет (удаление приватно, в notifications не уходит — INV-MSG-2). Записывается только намерение realtime-доставки для realtime-dispatch (persist-then-push → хаб MessageDeleted, группа chat:{ChatId}, нагрузка {ChatId, MessageId, DeletedAt}без прошлого тела).
  7. Результат. Tombstone-проекция MessageModel (IsDeleted = true, DeletedAt, Text/Attachment = null), 200.

Диаграмма

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

ТипДетали
Интеграционные событиянет (удаление в Kafka/notifications не публикуется — INV-MSG-2/приватность)
Внешние вызовынет (бинарь во внешнем storage не удаляется этой операцией — регламент storage/retention)
Проекции / поискденорм LastMessagePreview → «Сообщение удалено» (если удаляется последнее сообщение)
Уведомленияin-app realtime MessageDeleted (только участникам); offline — нет

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400пустой ChatId/MessageId«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
FORBIDDEN403нет permission messenger.messages.edit«Недостаточно прав»no retry
MSG_CHAT_ACCESS_DENIED403actor — не активный участник«Нет доступа к чату»no retry
MSG_EDIT_FORBIDDEN403не автор, или EventCard/системное (INV-MSG-1/4)«Удалить можно только своё сообщение»no retry
MSG_MESSAGE_NOT_FOUND404сообщение не найдено или ChatId не совпадает«Сообщение не найдено»no retry
MSG_EDIT_WINDOW_EXPIRED409now > EditableUntil (окно ≤ 60 с истекло)«Время для удаления истекло»no retry
MSG_EDIT_CONFLICT409оптимистический конфликт«Сообщение изменилось, повторите»retry (перечитать)

Acceptance Criteria

  • Удаление в окне. Дано своё Text/Attachment-сообщение и now <= EditableUntil. Когда DELETE. Тогда проставляется DeletedAt = now (тело/подпись скрыты на чтении), Seq сохранён, записан снимок MessageEditEntity, переход Sent/Edited → Deleted (INV-MSG-1), push MessageDeleted в chat:{ChatId}, ответ 200 tombstone.
  • Идемпотентный повтор. Дано сообщение уже удалено в окне. Когда повторный DELETE. Тогда 200 с текущим tombstone без изменений.
  • Окно истекло. Дано now > EditableUntil (> 60 с). Когда DELETE. Тогда MSG_EDIT_WINDOW_EXPIRED (409), сообщение иммутабельно.
  • Не автор. Дано SenderUserId != actor. Когда DELETE. Тогда MSG_EDIT_FORBIDDEN (403).
  • Карточка/системное. Дано Kind == EventCard или системное сообщение (INV-MSG-4). Когда DELETE. Тогда MSG_EDIT_FORBIDDEN (403).
  • Не участник. Дано актор не активный участник чата. Когда DELETE. Тогда MSG_CHAT_ACCESS_DENIED (403).
  • Сообщение не найдено. Дано MessageId не существует или ChatId не совпал. Когда DELETE. Тогда MSG_MESSAGE_NOT_FOUND (404).
  • Конкурентная правка. Дано параллельная правка того же сообщения. Когда обе доходят. Тогда проигравший получает MSG_EDIT_CONFLICT (409) и перечитывает.
  • Удаление последнего сообщения. Дано удаляется последнее сообщение чата. Когда DELETE. Тогда LastMessagePreview = "Сообщение удалено", LastMessageSeq/Seq не меняются.
  • Приватность. Kafka-события/уведомления в notifications нет (INV-MSG-2); прошлое тело, FileId, IP/UA не попадают в push/логи.

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

  • Модель ответа — та же union MessageModel (tombstone); изменения — additive без breaking. Окно удаления (60 с) — доменная константа EditableUntil. Мягкое удаление сохраняет Seq — физического удаления нет.
  • Logs: message-deleted (ChatId, MessageId, Revision, EditorUserId; прошлое тело/FileId/IP/UA не пишутся); metrics: messenger_messages_deleted_total{result}, latency p95 < 300 мс; traces через traceparent; dashboard pin-messenger.

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

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

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