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

Messenger Service

Назначение

messengerсобственный мини-мессенджер платформы PIN (ADR-0055). Владеет личными чатами 1:1, групповыми чатами CRM-процессов (чат лида — до сделки; чат сделки), типизированными сообщениями (текст / вложение / событие-карточка), контактами (заявка → подтверждение), чёрным списком, поиском пользователей по телефону, read-receipts / непрочитанными, а также аудио/видео-звонками, конференциями и демонстрацией экрана поверх внешнего media-стека (SFU, ADR-0062) с policy-driven записью звонков в file storage. Хранение — PostgreSQL (агрегаты Chat / Message / Participant / ChatEvent / Contact / Blacklist / CallSession / ConferenceSession / CallParticipant / CallRecording); медиа идёт клиент ↔ SFU (бэкенд минтит токен доступа и принимает вебхуки, не в медиа-плоскости); доставка в реальном времени — SignalR-hub (persist-then-push: outbox → hub).

Greenfield. Платформа реализует собственный REST + SignalR-контракт мессенджера с нуля (ADR-0055).

Metadata

ПолеЗначение
Статусdraft
Owning teamPIN Platform (Comms squad)
DomainКоммуникации / чаты
Data classificationMixed (PII: тексты сообщений, ФИО/телефоны контактов; Sensitive: IP/UA отправителя, скрытая история правок)
Runtime criticalityhigh (realtime UX; чаты сделок — часть воронки денег, но не source of truth сделки)

Ownership

ArtifactOwner/Rule
PostgreSQL schemamessenger
API areaschats / messages / contacts / blacklist / calls / conferences / recordings / internal (вебхуки media-стека) / admin (арбитраж Службы заботы) + SignalR-hub /hubs/messenger
Owned entitiesChatEntity(+ParticipantEntity), MessageEntity (TPH: TextMessage/AttachmentMessage/EventCardMessage; +MessageEditEntity, +EventCardRecipientEntity), ChatEventEntity, ContactEntity, BlacklistEntity, CallSessionEntity, ConferenceSessionEntity(+CallParticipantEntity), CallRecordingEntity
Replica entitiesMessengerUserEntity (реплика identity: ФИО/телефон для поиска и display контактов)
Produced eventspin.messenger.deal-chat-status-changed.v1, pin.messenger.message-posted.v1, pin.messenger.contact-request-created.v1, pin.messenger.contact-request-accepted.v1, pin.messenger.conference-ended.v1, pin.messenger.call-recording-available.v1
Consumed eventspin.crm.deal-created.v1, pin.crm.deal-stage-changed.v1, pin.crm.deal-members-changed.v1, pin.crm.lead-participants-changed.v1, pin.crm.document-requested.v1, pin.crm.booking-paid.v1, pin.crm.booking-expired.v1, pin.identity.users.v1
Search indexesElasticsearch-проекция messenger-contacts (ADR-0059; search/contacts.md) — FTS по ФИО/телефону контактов; source of truth — реплика MessengerUserEntity в PostgreSQL; PostgreSQL-FTS (tsvector/GIN) не используется
Analytics factsнет (ClickHouse не используется; simplicity-first ADR-0056; dev-stats — отдельный контекст)

Ответственность И Границы

  • Делает: личные чаты 1:1 (открытие по пользователю / из поиска; авто-создание при первом сообщении); групповые чаты CRM-процессов — лида (ChatKind.Lead, до сделки) и сделки (ChatKind.Deal) — с авто-созданием и синхронизацией участников по событиям crm (единый ParticipantRole, состав — производный от crm, пользовательских ручек управления составом нет); типизированные сообщения (текст; вложение = ссылка на FileId в storage, загрузка вне форм; событие-карточка с per-participant видимостью и per-participant текстами); авто-карточки целевых действий («Загрузите документы для бронирования/ДДУ/ДКП», «Прикрепите чек») и информационные («Договор бронирования оплачен», «Отправлено в Росреестр», «Бронь истекла — подтвердите/продлите»); правка/удаление сообщения ≤ 1 мин + хранимая (не показываемая) история правок; захват IP / User-Agent отправителя; read-receipts и счётчики непрочитанных; контакты (глобальный поиск по телефону с маской фамилии → заявка → подтверждение/отклонение → полный профиль обеим сторонам); чёрный список (запрет писать и звонить); 1:1-звонки (лог + приглашение с комнатой/токеном через hub) и групповые конференции в чатах сделок (динамический ростер, модератор, демонстрация экрана) поверх внешнего SFU (ADR-0062); policy-driven запись звонков с выгрузкой в file storage и историей; realtime-доставка через SignalR (persist-then-push).
  • Не делает: не владеет жизненным циклом сделки/участников (владелец — crm; messenger — консюмер событий, возвращает готовность чата событием); не хранит бинарные файлы, включая записи звонков (file storage/CDN, только FileId + метаданные); не в медиа-плоскости — медиа (звук/видео/экран) идёт клиент ↔ SFU, бэкенд минтит токен доступа и принимает вебхуки, SDP/ICE/токены не персистятся; не рассылает push/email напрямую (доставка — notifications как консюмер наших событий; SignalR — только in-app realtime); не источник справочника пользователей (реплика из identity).
  • Source of truth: Chat, Participant, Message (+edit history), ChatEvent, Contact, Blacklist, CallSession, ConferenceSession (+CallParticipant), CallRecording (метаданные; бинарь — file storage).
  • Реплики/проекции: MessengerUserEntity (ФИО/телефон из pin.identity.user-upserted, монотонный version-guard); snapshot названия/участников/проекта сделки — из событий crm при провизии/синке чата.

Зависимости

ЗависимостьТипЗачем НужнаFailure mode
PostgreSQL (messenger)dbвсе агрегаты, source of truth реплики/search-проекции контактов, outboxнедоступность = недоступность сервиса
Elasticsearch (search projection)searchFTS контактов по ФИО/телефону (messenger-contacts, ADR-0059) — read/search-проекция, не source of truthдеградация: поиск контактов недоступен/отстаёт; листинг контактов без search работает (PostgreSQL); rebuild-джоб восстанавливает
Kafkaqueueвходящие события crm/identity, исходящие (outbox-first)деградация: события копятся в outbox; чаты сделок отстают (ChatStatus = Provisioning/Failed)
identityservice/eventsJWT (actor/permissions), реплика ФИО/телефона (user-upserted) для контактов/поисканедоступность = 401/503 на командах; поиск/контакты отстают (eventual)
crmproducer событий для насжизненный цикл чата сделки: создание, синк участников, карточкичат сделки отстаёт; carousel карточек задержан (eventual)
storage/CDNexternalпресайн-ссылки вложений (FileId), скачиваниеотправка/просмотр вложений недоступны; текст работает
file storage (CALL_RECORDINGS)serviceвыгрузка и хранение записей звонков (S3), пресайн скачивания; ссылка по FileIdзапись не выгружается (recording-finalize-retry повторяет); чат/звонок работают
media-стек: SFU (JVB) + jicofo + prosodyexternal (ADR-0062)медиа-плоскость звонков/конференций, комнаты, JWT-авторизация; вебхуки admission/presenceзвонки/конференции недоступны (CALLS_MEDIA_ENABLED/CONFERENCE_CALLS_ENABLED off = graceful 503); чат/лог работают
coturn (TURN/STUN)externalобход NAT для клиент ↔ SFUчасть клиентов не соединяется по медиа; сигналинг/лог работают
jibri (рекордер)externalсерверная запись конференции → finalize → ingest в messenger → file storageзапись не ведётся/не финализируется; звонок работает без записи
notificationsconsumer наших событийoffline push/email о новых сообщениях, заявках в контакты, готовности записиуведомления вне приложения отстают (in-app realtime работает)
Redis (backplane)cacheSignalR backplane при multi-replica (ADR-0043/SDK) — опционально (на целевой нагрузке 1–10k активных один инстанс без Redis, ADR-0055/0056)без backplane при multi-replica часть push теряется → клиент перезапрашивает REST при reconnect

Проекты

src/services/messenger/
Pin.Messenger.Api/ # области chats/messages/contacts/blacklist/calls/conferences/recordings/internal/admin + SignalR-hub /hubs/messenger
Pin.Messenger.Application/ # сценарии/операции чатов, сообщений, контактов, звонков/конференций/записей (доступ по членству, не по tenant); RecordingPolicy; минтинг токена media-комнаты
Pin.Messenger.Domain/ # агрегаты, CommunicationPolicy, RecordingPolicy, инварианты, доменные события
Pin.Messenger.Infrastructure/ # хранение (PostgreSQL, TPH Message/ChatEvent), outbox, realtime-публикация (SignalR), storage/file-storage presign+upload-адаптеры, минтинг media-JWT, поиск-проекция контактов (Elasticsearch)
Pin.Messenger.Background/ # Kafka-консюмеры (crm/identity), воркеры (chat-provision-retry, call-timeout-reaper, conference-room-reaper, recording-finalize-retry, contact-search-rebuild), realtime-dispatch (outbox → hub)
Pin.Messenger.Migrator/ # миграции + SEED каталога типов карточек (EventCardType → i18n-ключи текстов целевых/инфо карточек)
Pin.Messenger.Contracts/ # контракты интеграционных событий pin.messenger.*; SignalR message-контракты (RealtimeMessage)

Основные Сущности

Детальные документы — domain/<entity>.md по entity-template, по одному файлу на агрегат (создаются следующим слоем ТЗ).

EntityНазначениеSource Of TruthДокумент
ChatEntity (+ParticipantEntity)Чат: личный 1:1 (DirectKey), лида (LeadId) или сделки (DealId); участники, read-state, denorm last-messagemessengerdomain/chat.md, domain/participant.md
MessageEntity (TPH)Сообщение: Text/Attachment/EventCard; edit ≤ 1 мин + скрытая история; IP/UA; per-participant видимость/тексты карточекmessengerdomain/message.md
ChatEventEntityСистемная лента чата: создан/переименован/участник добавлен/удалён/закрыт (append-only, без read-state)messengerdomain/chat-event.md
ContactEntityКонтакт-связь заявка → подтверждение (разрешение звонков + раскрытие профиля)messengerdomain/contact.md
BlacklistEntityЧёрный список (запрет писать/звонить)messengerdomain/blacklist.md
CallSessionEntity1:1-звонок: лог + медиа поверх SFU (SessionKind = Direct)messengerdomain/call-session.md
ConferenceSessionEntity (+CallParticipantEntity)Групповой звонок в чате сделки: ростер, модератор, демонстрация экранаmessengerdomain/conference-session.md, domain/call-participant.md
CallRecordingEntityЗапись звонка/конференции (policy auto-record → file storage FileId)messengerdomain/call-recording.md
MessengerUserEntityРеплика identity (ФИО/телефон) для поиска и display контактовidentity (реплика)domain/messenger-user.md

ER-обзор

API

Карта областей (детальные документы — api/<method>.md по api-method-template, по одному файлу на операцию; создаются следующим слоем ТЗ):

  • chats (/api/messenger/chats): list-chats (список чатов + unread + last-message-preview), get-chat (карточка + участники + read-state), open-direct-chat (создать/открыть личный 1:1, идемпотентно по паре), mark-chat-read (отметить прочитанным до Seq).
  • messages (/api/messenger/chats/{chatId}/messages): list-messages (история, cursor по Seq, per-participant фильтр карточек), send-text-message, send-attachment-message (FileId + caption), edit-message (PATCH ≤ 1 мин), delete-message (DELETE ≤ 1 мин, soft).
  • contacts (/api/messenger/contacts): list-contacts, search-users-by-phone (Имя Отчество + первая буква Фамилии), list-contact-requests, send-contact-request, accept-contact-request, decline-contact-request, remove-contact.
  • blacklist (/api/messenger/blacklist): list-blacklist, block-user, unblock-user.
  • calls (/api/messenger/calls, 1:1 медиа поверх SFU): start-call, answer-call, decline-call, end-call, list-calls (история / пропущенные), media-token (обновление токена комнаты).
  • conferences (/api/messenger/conferences и /chats/{chatId}/conferences): start-conference, join-conference, leave-conference, end-conference, get-conference, list-conferences, conference-media-token; управление составом — invite-participant (адресный ring), decline-conference, remove-participant (модератор) — групповые звонки в чатах сделок.
  • recordings (/api/messenger/recordings и .../{callId|conferenceId}/recordings): list-call-recordings, list-conference-recordings, get-recording, get-recording-download (пресайн file storage), stop-recording (модератор).
  • internal (/api/messenger/internal, вебхуки media-стека, ExternalSystem + подпись): jitsi-reservation (admission комнаты), jitsi-events (presence участников/записи), jibri-recording-ingest (приём байтов записи → file storage).
  • hub: MessengerHub (/hubs/messenger) — контракт SignalR (server → client push: сообщения, звонки, конференции, запись, экран + client → server ephemeral typing/join-chat).
  • admin (/api/messenger/admin, Служба заботы): get-chat-transcript (read-only арбитраж, включая историю правок).

Events И Consumers

TypeNameTopicНазначениеLink
ProducedDealChatStatusChangedEventpin.messenger.deal-chat-status-changed.v1готовность чата сделки → проекция Deal.ChatStatus в crm (Provisioning/Ready/Failed)events/deal-chat-status-changed.md
ProducedMessagePostedEventpin.messenger.message-posted.v1notifications: offline push/email адресатам (без тела — non-PII)events/message-posted.md
ProducedContactRequestCreatedEventpin.messenger.contact-request-created.v1notifications: уведомить адресата о заявке в контактыevents/contact-request-created.md
ProducedContactRequestAcceptedEventpin.messenger.contact-request-accepted.v1notifications: уведомить инициатора о подтвержденииevents/contact-request-accepted.md
ProducedConferenceEndedEventpin.messenger.conference-ended.v1crm (карточка встречи в таймлайн сделки), notifications, dev-statsevents/conference-ended.md
ProducedCallRecordingAvailableEventpin.messenger.call-recording-available.v1crm (прикрепить запись к сделке), notifications (запись готова)events/call-recording-available.md
Consumedpin.crm.deal-created.v1crmсоздать чат сделки, добавить участников, вернуть готовностьconsumers/deal-created.md
Consumedpin.crm.deal-stage-changed.v1crmкарточка «этап изменён»; на терминальный ToStatus — финальная инфо-карточка + закрытие чатаconsumers/deal-stage-changed.md
Consumedpin.crm.deal-members-changed.v1crmидемпотентная set-реконсиляция участников чата сделки (ActiveMembers)consumers/deal-members-changed.md
Consumedpin.crm.lead-participants-changed.v1crmидемпотентная set-реконсиляция участников чата лида ChatKind.Lead (ActiveParticipants)consumers/lead-participants.md
Consumedpin.crm.document-requested.v1crmкарточка целевого действия «Загрузите документы» (Pending, адресат — инвестор)consumers/document-requested.md
Consumedpin.crm.booking-paid.v1crmинфо-карточка «Договор бронирования оплачен» + суммаconsumers/booking-paid.md
Consumedpin.crm.booking-expired.v1crmкарточка «Бронь истекла — подтвердите/продлите»consumers/booking-expired.md
Consumedpin.identity.users.v1identityреплика ФИО/телефона (MessengerUserEntity) + реиндексация search-документа контактов (Elasticsearch)consumers/identity-user-upserted.md

Все produced-события публикуются через транзакционный outbox (несут ContractVersion, версия — суффикс топика .vN); все консюмеры идемпотентны по идентификатору сообщения + бизнес-ключу. Источники consumed-событий: crm, identity.

Кросс-сервисные контракты (ожидания и гарантии)

Что messenger ожидает от соседей (входящие события):

  • От crm (жизненный цикл чата сделки): ключ партиционирования = DealId (per-deal порядок), доставка at-least-once, deal-created предшествует deal-stage-changed/deal-members-changed/booking-* той же сделки. Снапшот deal-created несёт DealId, developer-company (DealTenantId), проект, состав участников (Members), стадию — этого достаточно для провизии чата без синхронных дозапросов. При лаге событий чат сделки отстаёт (ChatStatus = Provisioning), не теряется.
  • От crm (состав чата лида): ключ = LeadId (per-lead порядок), доставка at-least-once; lead-participants-changed несёт полный активный состав ActiveParticipants + дельту и монотонную Versionmessenger приводит ростер чата лида (ChatKind.Lead) к присланному составу set-реконсиляцией (consumers/lead-participants.md). Провизия Lead-чата — по факту создания лида (аналог deal-created для сделки, соседний слой ТЗ); до провизии синк состава ретраится (MSG_CHAT_NOT_PROVISIONED).
  • От identity (реплика пользователей): топик pin.identity.users.v1, ключ = UserId (per-user порядок), монотонная доменная Version (version-guard у потребителя — старое событие игнорируется), at-least-once. Несёт ФИО/телефон — источник display-имени и поиска контактов. При отсутствии реплики (первое событие ещё не доехало) read-модели отдают безопасный placeholder, строку не отбрасывают.

Что messenger предоставляет (исходящие события): каждое produced-событие идёт через транзакционный outbox, несёт ContractVersion (сейчас 1) и партиционируется бизнес-ключом ради порядка: deal-chat-status-changed и conference-ended — по DealId; message-posted — по ChatId; call-recording-available — по SessionId; contact-request-* — по адресату. Совместимость additive-only (новые optional-поля — без bump; breaking — новый ContractVersion/.vN + dual-publish). Потребители обязаны быть идемпотентны (событие at-least-once).

Окна eventual-consistency и промежуточные состояния для пользователя: провизия чата сделки deal-created → Ready — целевое окно < 5 мин (ChatStatus: Provisioning → Ready, при исчерпании ретраев — Failed + re-emit воркером chat-provision-retry); пользователь в промежутке видит «чат готовится». Рассинхрон реплики пользователя (лаг user-upserted) — секунды/минуты: контакт/участник показывается с placeholder-подписью до догона. Search-проекция контактов (Elasticsearch) отстаёт от source of truth на секунды; листинг контактов без поиска работает по PostgreSQL немедленно.

Runtime И Эксплуатация

ОбластьТребование
Health checks/health/live, /health/ready (PostgreSQL + Kafka + Elasticsearch (search контактов, non-fatal degrade) + file storage + Redis-backplane если включён; media-стек/SFU — non-fatal degrade, звонки за флагом), /health/startup (миграции применены)
SLO/SLApersist команды сообщения p95 < 300 мс; realtime push (persist → hub) p95 < 1 с; списки/история p95 < 500 мс; провизия чата сделки (deal-created → Ready) < 5 мин; вход в звонок/конференцию (join) p95 < 3 с; успешность записи ≥ 99%; hub KeepAlive 15 с / ClientTimeout 30 с / MaximumReceiveMessageSize 32 КБ
Background jobschat-provision-retry (цикл 1 мин; ChatStatus = Failed → повтор + re-emit), call-timeout-reaper (цикл 15 с; Ringing без ответа > 45 с → Missed), conference-room-reaper (цикл 30 с; пустая/зависшая конференция → Ended/Failed), recording-finalize-retry (цикл 60 с; застрявшая выгрузка записи → сверка file storage / Failed), realtime-dispatch (hosted service: outbox → hub relay, persist-then-push), contact-search-rebuild (bootstrap/восстановление индекса messenger-contacts, ADR-0059)
DashboardGrafana pin-messenger (создаётся при реализации); отдельные панели JVB/jibri/coturn (эксплуатация media-стека — calls/jitsi-deployment.md)
Alertsлаг консюмеров > 5 мин; outbox backlog > 1000; провизия чата Failed > 10/ч; всплеск ошибок realtime-dispatch; hub disconnect rate; запись Failed/застряла > 30 мин; насыщение рекордеров jibri
Runbooksdocs/06-services/messenger/runbooks/ (будущий путь по конвенции); эксплуатация media-стека — calls/
Feature flagsREALTIME_BACKPLANE_ENABLED (Redis backplane для multi-replica); CALLS_MEDIA_ENABLED (медиа 1:1-звонков поверх SFU; заменяет исторический CALLS_SIGNALING_ENABLED); CONFERENCE_CALLS_ENABLED (групповые конференции); RECORDINGS_ENABLED (запись звонков; авто-старт — RecordingPolicy)

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

  • Actor types: Инвестор, Агент, Руководитель агентства, Менеджер застройщика, Менеджер ПИН (все User), Служба заботы (read-only арбитраж), система (консюмеры / hub / воркеры).
  • Permission model: Scoped-RBAC + record-level (permission); permissions messenger.chats.view/.write, messenger.messages.edit, messenger.contacts.manage/.search, messenger.blacklist.manage, messenger.calls.use, messenger.conferences.use, messenger.recordings.view/.manage, messenger.admin.view (арбитраж, platform-scope). Вебхуки media-стека (internal) — actor ExternalSystem с подписью (HMAC/mTLS), fail-closed по имени комнаты.
  • Tenant isolation (ADR-0052): чаты messenger НЕ используют строковую single-tenant изоляцию (личные чаты кросс-стороны: инвестор ↔ агент, инвестор ↔ инвестор; инвестор — вне tenant). Доступ = record-level по членству (активный ParticipantEntity). Чат сделки несёт DealTenantId = DeveloperCompanyId только для корреляции и scope арбитража Службы заботы.
  • Sensitive/PII: тексты сообщений (PII); ФИО/телефон контакта (PII; в поиске фамилия маскируется первой буквой до подтверждения контакта); IP/UA отправителя (Sensitive, только internal — наружу и в события/логи не отдаются); скрытая история правок (не отдаётся ни одной read-моделью, кроме admin-арбитража). События не несут тела сообщений/PII (только id).
  • Audit: версия на агрегатах; бизнес-история — append-only ChatEvent + MessageEdit; блокировки/контакты — статусные переходы.

Известные Ограничения И Assumptions

  • Simplicity-first (ADR-0056): PostgreSQL — source of truth; таймеры/reaper — polling-воркеры без Redis TTL; целевая нагрузка ≤ 1 млн пользователей, 1–10k одновременно активных → один инстанс, backplane опционален. Поиск контактов — Elasticsearch-проекция (ADR-0059 supersedes FTS-пункт ADR-0056), а не PostgreSQL-FTS.
  • Media-стек звонков (SFU/TURN/рекордер) — self-hosted Jitsi (ADR-0062): JVB=SFU, coturn=TURN, jibri=запись; бэкенд минтит токен доступа и принимает вебхуки, медиа идёт клиент ↔ SFU (не в медиа-плоскости). Запись — policy-driven, выгрузка в file storage. Инфраструктура/интеграция — calls/.
  • Запись хранится в file storage по FileId (бинарь не в messenger); транскрипция/конспект — будущая фаза (шов через call-recording-available, calls/transcription-roadmap.md).
  • Правка/удаление жёстко ≤ 60 с (EditableUntil), затем immutable; история правок хранится, но не показывается (кроме admin-арбитража).
  • Realtime — не source of truth: при reconnect клиент перезапрашивает REST.

Принятые решения контекста

#ВопросРешение
1Транспорт realtimeSignalR, persist-then-push (outbox → hub); Redis backplane — опция (ADR-0055/0043)
2Оркестрация чат ↔ сделкамодуль CRM + события; messenger — консюмер; готовность возвращает deal-chat-status-changed
3Модель сообщенийEF TPH: TextMessage/AttachmentMessage/EventCardMessage; событие-карточка — per-participant видимость + per-participant тексты (child EventCardRecipient), typed jsonb payload
4Кто кому может писать/звонитьCommunicationPolicy: писать можно не-контакту, запрет только по чёрному списку (любая сторона); звонить и видеть полный профиль — только подтверждённый контакт
5Доступ к чатупо членству (активный Participant), не по tenant; чат сделки хранит DealTenantId для арбитража
6Непрочитанные / read-receiptsмонотонный per-chat Seq; Participant.LastReadSeq; unread = Chat.LastMessageSeq − LastReadSeq
8Вложениятолько FileId + метаданные (storage/CDN); бинарь не хранится; загрузка вне форм
9Звонки1:1 + групповые конференции поверх self-hosted Jitsi (JVB SFU, ADR-0062); бэкенд минтит токен и room-authority, медиа клиент ↔ SFU; SDP/ICE/токены не персистятся (CallSessionEntity/ConferenceSessionEntity)
10Запись звонковpolicy-driven auto-record (RecordingPolicy); jibri → ingest в messenger → file storage (FileId); история + будущая транскрипция; crm/notifications через call-recording-available

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