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.
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | messages |
| Feature group | messages |
| Статус | draft |
| Документ | docs/06-services/messenger/api/messages/delete-message.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (автор сообщения) |
| Auth policy / Permissions | authenticated (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) |
| Audit | soft-delete (MessageEntity.DeletedAt) + append MessageEditEntity (снимок прошлого, EditorUserId = actor); IP/UA — Sensitive/internal |
HTTP-контракт
| Поле | Значение |
|---|
| Method | DELETE |
| Route | /api/messenger/chats/{chatId}/messages/{messageId} |
| Success status | 200 (tombstone-проекция; повтор на уже удалённом в окне — тоже 200, идемпотентно) |
| Idempotency header | не применяется (мягкое удаление естественно идемпотентно; конкуренция — оптимистическая блокировка) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ChatId | route | Guid | Да | != Guid.Empty; чат существует; actor — активный участник | — |
MessageId | route | Guid | Да | != Guid.Empty; сообщение существует, ChatId совпадает; SenderUserId = actor; now <= EditableUntil; Kind != EventCard (см. Алгоритм) | — |
UserId | identity claims | Guid | Да | Required; предикат членства + авторства | scope isolation |
IpAddress | server (remote/XFF) | IpAddress | Да | захват сервером; снимок удаления | Sensitive/internal |
UserAgent | header User-Agent | string | Да | <= 512; захват сервером | Sensitive/internal |
Тело запроса пустое: удаление адресуется только по route-идентификаторам.
Модель ответа MessageModel
Union-проекция удалённого сообщения (как в list-messages.md): IsDeleted = true, тело/
подпись не отдаются (под-модели Text/Attachment = null), Seq сохранён для порядка ленты. История
правок и IP/UA не входят (INV-MSG-2).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | MessageEntity.Id | — | Идентификатор сообщения. |
Seq | long | Да | Seq | — | Порядок в ленте (сохраняется — строка не физически удалена). |
Kind | string (MessageKind) | Да | Kind | — | Text / Attachment. |
IsDeleted | bool | Да | DeletedAt != null | — | Всегда true; клиент рендерит «сообщение удалено». |
DeletedAt | DateTimeOffset | Да | DeletedAt | — | Момент удаления. |
Text | MessageTextModel? | Нет | — | скрыто | Всегда null (тело скрыто). |
Attachment | MessageAttachmentModel? | Нет | — | скрыто | Всегда null (метаданные/FileId скрыты). |
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь-автор; требуется право
messenger.messages.edit
(иначе — FORBIDDEN; без JWT — UNAUTHORIZED). ChatId/MessageId берутся из маршрута; IpAddress/
UserAgent захватываются сервером.
- Проверка входа и предусловий.
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, без изменений).
- Что читаем. Сообщение (для проверок и снимка); при удалении последнего сообщения чата дополнительно
сам чат — для денормализации
LastMessagePreview.
- Бизнес-правила и переход. Рассчитать
Revision = (max prior)+1; записать снимок прошлого контента в
MessageEditEntity (PreviousBody/PreviousCaption, EditorUserId = actor, EditedAt = now, IpAddress/
UserAgent); проставить DeletedAt = now (тело/подпись скрываются на чтении). Переход состояния
Sent/Edited → Deleted (INV-MSG-1). Если удаляется последнее сообщение чата — LastMessagePreview = "Сообщение удалено" (LastMessageSeq/Seq не меняются).
- Что меняется. Обновляется сообщение (мягкое удаление) + добавляется снимок
MessageEditEntity (+
обновляется чат при денормализации). Всё фиксируется в одном согласованном изменении (INV-MSG-2);
оптимистический конфликт → MSG_EDIT_CONFLICT (перечитать).
- Факты и события. Интеграционного Kafka-события нет (удаление приватно, в
notifications не уходит —
INV-MSG-2). Записывается только намерение realtime-доставки для realtime-dispatch (persist-then-push →
хаб MessageDeleted, группа chat:{ChatId}, нагрузка {ChatId, MessageId, DeletedAt} — без прошлого
тела).
- Результат. 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_FAILED | 400 | пустой ChatId/MessageId | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
FORBIDDEN | 403 | нет permission messenger.messages.edit | «Недостаточно прав» | no retry |
MSG_CHAT_ACCESS_DENIED | 403 | actor — не активный участник | «Нет доступа к чату» | no retry |
MSG_EDIT_FORBIDDEN | 403 | не автор, или EventCard/системное (INV-MSG-1/4) | «Удалить можно только своё сообщение» | no retry |
MSG_MESSAGE_NOT_FOUND | 404 | сообщение не найдено или ChatId не совпадает | «Сообщение не найдено» | no retry |
MSG_EDIT_WINDOW_EXPIRED | 409 | now > EditableUntil (окно ≤ 60 с истекло) | «Время для удаления истекло» | no retry |
MSG_EDIT_CONFLICT | 409 | оптимистический конфликт | «Сообщение изменилось, повторите» | 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы