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

POST /api/messenger/conferences/{conferenceId}/invite — Пригласить участников в конференцию

Активный участник идущей конференции адресно зовёт конкретных членов Deal-чата. Для каждого приглашённого заводится/реактивируется строка ростера (call-participant.md) в Invited, и лично ему (user:{UserId}) уходит ConferenceInvite с дескриптором входа в комнату. Дом полей, статусы и инварианты — conference-session.md.

Назначение

Дать участнику встречи по сделке позвать в идущую конференцию конкретных коллег из того же Deal-чата (адресный «дозвон»), не поднимая новую сессию. На выходе — сводка приглашения: кого позвали и кого пропустили (уже в комнате / уже приглашён / не участник чата). Отличается от стартового ConferenceInvite: старт рассылает приглашение broadcast в группу chat:{ChatId}, этот метод шлёт адресный ring в user:{UserId} каждому цели. Owning-сервис — messenger, feature group conferences, сценарий «групповой звонок по сделке».

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areaconferences
Feature groupconferences
Статусdraft
Документdocs/06-services/messenger/api/conferences/invite-participant.md

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

ПроверкаЗначение
Actor typeUser — активный участник Deal-чата конференции
Auth policy / Permissionspermission messenger.conferences.use
Scope (RBAC)не применяется — доступ по членству в чате
Record-levelактивный ParticipantEntity чата ChatId конференции (INV-CHAT-4); каждая цель — тоже активный участник этого чата
Tenant isolationне применяется (кросс-tenant); срез — по членству в Deal-чате
Auditcreate/update ростера: строки целей → Invited, метка InvitedAt

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/conferences/{conferenceId}/invite
Success status200 (OK)
Idempotency headerне применяется — повтор идемпотентен по строке ростера: цель уже Invited/Joined → в Skipped, дубля не создаётся (INV-CPART-1)
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ConferenceIdrouteGuidДа!= Empty; существующая конференция в Active (шаги 3–4)
ActorUserIdidentity claimsGuidДаиз JWT; активный участник Deal-чата конференции
UserIdsbodyGuid[]Да1..N непустых уникальных Guid; пустой список / все Empty → ошибка валидации

UserIds — цели приглашения (члены того же Deal-чата). Дубли в списке схлопываются; порядок ответа не гарантируется.

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

Сводка адресного приглашения: кого позвали и кого пропустили с причиной. Никогда не «сырой» массив — всегда объект-обёртка (расширяемость без breaking).

ПолеТипОбязательностьИсточникMaskingОписание
ConferenceIdGuidДаConferenceSessionEntity.IdКонференция, в которую звали.
InvitedGuid[]ДаростерЦели, для которых заведена/реактивирована строка Invited и отправлен адресный ConferenceInvite.
SkippedInviteSkippedEntry[]ДавычислениеЦели, пропущенные без приглашения, с причиной. Пустой массив — все позваны.

Вложенная модель InviteSkippedEntry

ПолеТипОбязательностьИсточникMaskingОписание
UserIdGuidДавходПропущенная цель.
ReasonInviteSkipReasonДавычислениеПричина пропуска (см. enum).

Enum InviteSkipReason

ЗначениеОписание
AlreadyJoinedЦель уже активна в комнате (State = Joined) — повторный ring не нужен.
AlreadyInvitedЦель уже в статусе Invited — приглашение уже висит, дубля не шлём.
NotChatMemberЦель не активный участник Deal-чата — пригласить в конференцию по сделке нельзя.

Дескриптор входа Room (MediaJoinModel) в этот ответ не входит — он уходит адресно в ConferenceInvite каждой приглашённой цели (токен минтится при доставке), а не инициатору приглашения.

Предусловия и постусловия

Предусловия. Актор авторизован и имеет messenger.conferences.use; флаг CONFERENCE_CALLS_ENABLED включён. Конференция существует и Active (INV-CONF-1/2); актор — активный участник её Deal-чата (INV-CHAT-4).

Постусловия. Для каждой цели-члена чата, ещё не Invited/Joined, заведена/реактивирована строка ростера State = Invited, ConferenceRole = Speaker, InvitedAt (INV-CPART-1); поднят ConferenceParticipantInvitedEvent. В той же транзакции на каждую приглашённую цель записана адресная строка realtime_outbox ConferenceInvite → после коммита realtime-dispatch пушит ConferenceInvite в user:{UserId} и минтит Room.AccessToken при доставке. Цели уже в комнате / уже приглашённые / не члены чата — в Skipped, состояние не меняется. ActiveParticipantCount приглашение не меняет (цель ещё не вошла). Kafka-события приглашение не публикует.

Алгоритм

  1. Контекст и доступ. Операцию выполняет активный участник Deal-чата (User); ActorUserId — из JWT; требуется право messenger.conferences.use. Конференции включены флагом CONFERENCE_CALLS_ENABLED, иначе — MSG_CONFERENCES_DISABLED.
  2. Проверка входа. ConferenceId непустой; UserIds — непустой список валидных уникальных Guid (1..N); иначе ошибка валидации.
  3. Загрузка сессии. Конференция по ConferenceId; нет → MSG_CONFERENCE_NOT_FOUND. Требуется статус Active; иначе MSG_CONFERENCE_NOT_ACTIVEProvisioning/Ended/Failed звать некого).
  4. Доступ актора. Актор — активный участник Deal-чата конференции (INV-CHAT-4); иначе MSG_CHAT_ACCESS_DENIED.
  5. Классификация целей. Загружается активный состав Deal-чата и текущий ростер конференции. Для каждой цели: не активный участник чата → Skipped(NotChatMember); уже JoinedSkipped(AlreadyJoined); уже InvitedSkipped(AlreadyInvited); иначе — приглашаемая. Если ни одна цель не приглашаема из-за членства (весь список — не участники чата) → MSG_CONFERENCE_TARGET_NOT_MEMBER (пустой ring не имеет смысла); смешанный список с хотя бы одной приглашаемой целью выполняется частично (не-члены попадают в Skipped).
  6. Приглашение. Для каждой приглашаемой цели заводится/реактивируется строка ростера (State = Invited, ConferenceRole = Speaker, InvitedAt, INV-CPART-1) и поднимается ConferenceParticipantInvitedEvent. В том же согласованном изменении фиксируется намерение доставки адресного ConferenceInvite (persist-then-push): после коммита realtime-dispatch пушит ConferenceInvite в user:{UserId} и минтит Room.AccessToken в момент доставки. Все строки и намерения — одна транзакция (атомарно).
  7. Результат. InviteResultModel с Invited (кому ушёл ring) и Skipped (кого и почему пропустили) → HTTP 200 OK.

Диаграмма

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

ТипДетали
Integration eventsnone — адресное приглашение Kafka не публикует
Realtime push (hub)ConferenceInviteuser:{UserId} каждой приглашённой цели (комната + токен, messenger-hub.md); адресный, а не broadcast в chat:{ChatId} (тот — при старте)
Токены доступаRoom.AccessToken/TURN-креды минтятся при доставке инвайта (realtime-dispatch), в realtime_outbox/БД не хранятся, не логируются
Cache / projections / searchростер (ix_call_participants_active не затрагивается — Invited ещё не активен)
Notificationsoffline push «вас зовут на встречу» — вне конференс-операции (потребитель hub/notifications)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400ConferenceId = Empty, пустой/невалидный UserIds«Проверьте параметры приглашения»no retry
MSG_CONFERENCE_NOT_FOUND404конференции нет«Конференция не найдена»no retry
MSG_CONFERENCE_NOT_ACTIVE409конференция не Active (Provisioning/Ended/Failed)«Конференция не активна»no retry (перечитать)
MSG_CHAT_ACCESS_DENIED403актор не активный участник Deal-чата«Нет доступа к конференции»no retry
MSG_CONFERENCE_TARGET_NOT_MEMBER409ни одна цель не активный участник Deal-чата (весь список — не члены)«Приглашаемый не участник чата сделки»no retry
MSG_CONFERENCES_DISABLED503CONFERENCE_CALLS_ENABLED = false«Конференции временно недоступны»retry позже

Acceptance Criteria

  • Адресный ring (happy). Дано: актор — активный участник Deal-чата Active-конференции, цель — тоже член чата, ещё не в ростере. Когда POST /invite {userIds:[цель]}. Тогда 200, строка цели → Invited (InvitedAt), Invited = [цель], Skipped = []; цель получает ConferenceInvite в user:{цель} с MediaJoinModel (токен минтится при доставке).
  • Частичный батч. Дано: список из четырёх целей — новый член, уже Joined, уже Invited, не член чата. Когда POST /invite. Тогда 200, Invited = [новый], Skipped = [{AlreadyJoined},{AlreadyInvited}, {NotChatMember}]; повторных/лишних ring нет.
  • Идемпотентность. Дано: цель уже Invited (или Joined). Когда актор зовёт её снова. Тогда 200, цель в Skipped(AlreadyInvited/AlreadyJoined), дубль строки не создаётся (INV-CPART-1), лишнего push нет.
  • Все цели — не члены чата. Дано: весь UserIds — не участники Deal-чата. Когда POST /invite. Тогда 409 MSG_CONFERENCE_TARGET_NOT_MEMBER, ростер не меняется.
  • Конференция не активна. Дано: конференция Ended/Provisioning/Failed. Когда POST /invite. Тогда 409 MSG_CONFERENCE_NOT_ACTIVE.
  • Не участник чата (актор). Дано: актор не активный участник Deal-чата. Когда POST /invite. Тогда 403 MSG_CHAT_ACCESS_DENIED.
  • Невалидный вход. Дано: пустой UserIds или ConferenceId = Empty. Тогда 400 VALIDATION_FAILED.
  • Флаг выключен. Дано: CONFERENCE_CALLS_ENABLED = false. Тогда 503 MSG_CONFERENCES_DISABLED.

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

  • Новые optional поля ответа — additive; смена маршрута/семантики Skipped — breaking. Kafka-контракта нет.
  • Logs: conference invite requested/invited/skipped (ConferenceId, ActorUserId, счётчики invited/skipped; токены — никогда в логи); metrics: messenger_conference_invite_total{result}, messenger_conference_invite_targets{outcome}, latency p95; traces — сквозной trace (traceparent); dashboard pin-messenger.

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

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

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