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

Event Contract: pin.messenger.message-posted (MessagePostedEvent)

Версионируемый контракт интеграционного события «в чат отправлено сообщение». Consumer-документы (notifications) ссылаются сюда, не переописывая payload. Событие фиксирует факт публикации сообщения ([*] → Sent, INV-MSG-3) и несёт корреляционные id + bounded превью — notifications рассылает offline push/email адресатам без синхронного запроса в messenger и без тела/PII. Дом полей сообщения — message.md; дом чата/участников — chat.md, participant.md.

Назначение

messenger доставляет сообщения in-app через SignalR (persist-then-push); для получателей офлайн push/email рассылает notifications как консюмер этого события. Событие сознательно не несёт список получателей и тело сообщения: адресаты выводятся notifications из собственной модели (активные участники чата минус отправитель, с учётом Participant.IsMuted), а тело — PII (не покидает границу messenger). Публичное как интеграционный триггер уведомлений; класс данных — Internal.

Metadata

ПолеЗначение
Event typepin.messenger.message-posted
Класс событияMessagePostedEvent
Topicpin.messenger.message-posted.v1
Partitions / key6 / key = ChatId (порядок сообщений внутри чата per-partition)
ContractVersion1
Producermessenger: отправка сообщения (send-text-message / send-attachment-message, дом message.md)
Статусdraft

Событие публикуется атомарно (в одном согласованном изменении) с фиксацией сообщения и денормом чата (Chat.RegisterPosted, INV-CHAT-5) через транзакционный outbox. Партиционирование по ключу ChatId (не по id сообщения) — так сообщения одного чата попадают в один раздел и обрабатываются notifications по порядку.

Payload MessagePostedEvent

Корреляционные поля + bounded-превью (для UX уведомления). Тело сообщения (Body/Caption) и Sensitive-поля (IpAddress/UserAgent) в payload запрещены (INV-MSG-6). Дом полей — message.md.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
ChatIdGuidДаMessage.ChatIdImmutableЧат-лента (Kafka key). notifications резолвит адресатов по составу.
ChatKindstring (enum ChatKind)ДаChat.KindAdditive onlyDirect / Deal — тексты/группировка уведомления.
MessageIdGuidДаMessage.IdImmutableСообщение; бизнес-ключ идемпотентности.
SeqlongДаMessage.SeqImmutable semanticsPer-chat монотонный порядковый (порядок/дедуп у consumer-а).
Kindstring (enum MessageKind)ДаMessage.KindAdditive onlyText / Attachment / EventCard.
SenderUserIdGuid?НетMessage.SenderUserIdImmutableАвтор; null — система/EventCard (self-notify исключается по нему).
SenderRolestring? (enum ParticipantRole)НетMessage.SenderRoleAdditive onlyРоль автора на момент отправки.
PreviewstringДаderivedAdditive onlyBounded превью (≤256): Text — обрезка тела; AttachmentFileName/класс; EventCardDefaultText. Не полный текст.
SentAtDateTimeOffsetДаMessage.SentAtImmutable semanticsМомент отправки (тайминг/группировка уведомления).

ChatId/MessageId иммутабельны; Preview — bounded-производное, а не тело (правки сообщения в окне ≤60с события повторно не публикуют — уведомление одноразовое по факту Sent).

Когда публикуется

УсловиеПереход состоянияСогласованность
Пользователь отправил Text/Attachment (Chat.Status = Active, CommunicationPolicy.CanMessage)[*] → Sentсообщение, Chat.RegisterPosted и факт отправки — в одном согласованном изменении (INV-CHAT-5 / INV-MSG-3)

Правка/удаление в окне (→ Edited/Deleted) и системные EventCard-карточки от консюмеров crm событие не публикуют: уведомление привязано к факту первичной отправки пользовательского сообщения. EventCard рассылаются собственными триггерами crm/notifications, не через этот топик.

Потребители (consumers)

Consumer-контекстЭффект (событие → что меняется)ИдемпотентностьЗадержка (SLA)
notificationsFan-out offline push/email адресатам (состав + IsMuted — из собственной проекции notifications; тело не входит, only-preview)first-seen по MessageId< 1 мин

notifications подавляет self-notify (recipient == SenderUserId) и замьюченных участников; отправитель и online-получатели уже получили сообщение через SignalR-hub (persist-then-push, не через это событие).

Гарантии доставки, порядок и обработка отказов

  • Порядок per-ChatId. Ключ ChatId (не MessageId) кладёт все сообщения одного чата в один раздел — notifications обрабатывает их по возрастанию Seq. Между чатами порядка нет.
  • At-least-once + идемпотентность. Доставка хотя бы раз; notifications дедуплицирует по MessageId (first-seen) — повторная доставка не задваивает offline-push/email.
  • Fan-out по реплике потребителя. Список адресатов и IsMuted notifications берёт из собственной проекции состава чата — она eventual относительно messenger. В узком окне возможен push только что вышедшему участнику или пропуск только что добавленного; расхождение самокорректируется по мере догона реплики состава и не блокирует доставку остальным.
  • Только первичная отправка порождает событие. Правка/удаление (→ Edited/Deleted) и системные EventCard событие не публикуют — уведомление одноразовое по факту Sent; EventCard рассылаются собственными триггерами crm/notifications.
  • Retry/DLQ. Транзиентный сбой consumer-а — ретрай по offset; poison после исчерпания попыток → DLQ + алерт, offset продвигается (offline-уведомление может опоздать; само сообщение доставлено online через hub и доступно по REST).

Версионирование и совместимость

  • ContractVersion = 1; при несовпадении версии входящего события с текущей consumer логирует warning и продолжает (апкаст схемы не выполняется). Топик кодирует версию суффиксом .v1 по конвенции.
  • Additive-only для minor: новые поля — optional с безопасным default, без bump.
  • Breakingновый ContractVersion = 2 + топик pin.messenger.message-posted.v2 + dual-publish + deadline миграции consumers + sunset .v1.
  • Unknown enum Kind/ChatKind/SenderRole — fallback: сохранить строку как есть + warning (уведомление деградирует до generic-текста, не fail).
  • Порядок сообщений одного чата гарантирован ключом ChatId; consumer держит дедуп по MessageId.

Безопасность

ПолеКласс данныхПравило
ChatId, ChatKind, MessageId, Seq, Kind, SenderUserId, SenderRole, SentAtInternalinclude (корреляция; не публичный контент)
PreviewInternal (bounded)include с жёсткой обрезкой ≤256 и без полного тела вложения
Body / Caption (тело)PIIexclude — не покидает границу messenger (INV-MSG-6)
IpAddress / UserAgentSensitiveexclude — не в события/логи (INV-MSG-6)

Событие несёт только bounded-превью, id и enum-статусы. НЕ содержит полного тела, ФИО/телефонов, IP/UA, secrets, tokens, платёжных данных, бинаря вложений.

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

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

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