POST /api/messenger/conferences/{conferenceId}/participants/{userId}/remove — Удалить участника из конференции
Модератор конференции исключает активного участника. Его строка ростера
(call-participant.md) переводится Joined → Left с
LeaveReason = RemovedByModerator, media-стеку отдаётся команда выкинуть окупанта из комнаты (kick),
ActiveParticipantCount пересчитывается. Дом полей, статусы и инварианты —
conference-session.md.
Назначение
Дать модератору встречи убрать участника из идущей конференции (нарушитель, ошибочно приглашённый, лишний):
зафиксировать выход по причине RemovedByModerator и физически выселить его из медиа-комнаты, чтобы он не
слышал/не видел встречу. На выходе — актуальная карточка конференции. Свой собственный выход модератор делает
через leave-conference.md, а не этим методом. Owning-сервис — messenger, feature group
conferences.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | messenger |
| API area | conferences |
| Feature group | conferences |
| Статус | draft |
| Документ | docs/06-services/messenger/api/conferences/remove-participant.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — модератор конференции (ConferenceRole == Moderator) |
| Auth policy / Permissions | permission messenger.conferences.use |
| Scope (RBAC) | не применяется — доступ по членству в чате + роль модерации |
| Record-level | активная строка ростера актора с ConferenceRole = Moderator (INV-CONF-6); членство в Deal-чате (INV-CHAT-4) |
| Tenant isolation | не применяется (кросс-tenant); срез — по членству |
| Audit | update ростера цели: Joined → Left, метки LeftAt/LeaveReason = RemovedByModerator, DurationSeconds |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/messenger/conferences/{conferenceId}/participants/{userId}/remove |
| Success status | 200 (OK) |
| Idempotency header | не применяется — повтор на уже Left-строке no-op (200, цель не активна) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ConferenceId | route | Guid | Да | != Empty; существующая конференция в Active, актор — модератор (шаги 3–4) | — |
UserId | route | Guid | Да | != Empty; активный (Joined) участник конференции (шаг 5) | — |
ActorUserId | identity claims | Guid | Да | из JWT; строка ростера актора с ConferenceRole = Moderator | — |
Тела запроса нет. LeaveReason не передаётся клиентом — фиксируется сервером как RemovedByModerator.
Модель ответа ConferenceModel
Проекция ConferenceSessionEntity с обновлённым ростером (дом полей —
start-conference.md). Поле Room отсутствует (удаление не выдаёт токен модератору —
он уже в комнате).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
ConferenceId | Guid | Да | ConferenceSessionEntity.Id | — | Идентификатор конференции. |
Status | ConferenceStatus | Да | .Status | — | Обычно ещё Active. |
ActiveParticipantCount | int | Да | .ActiveParticipantCount | — | Активных после удаления. |
Participants | ConferenceParticipantEntry[] | Да | ростер (call-participant.md) | — | Удалённая цель — IsActive = false, LeftAt, DurationSeconds. |
Полный состав полей карточки — ConferenceModel в start-conference.md; здесь — значимая
для удаления дельта, Room = null.
Предусловия и постусловия
Предусловия. Актор авторизован и имеет messenger.conferences.use; активный участник Deal-чата (INV-CHAT-4)
и модератор конференции (ConferenceRole = Moderator, INV-CONF-6). Конференция Active. Цель {userId} —
активный (Joined) участник этой конференции.
Постусловия. Строка ростера цели — State = Left, LeftAt, LeaveReason = RemovedByModerator,
DurationSeconds = LeftAt − JoinedAt (≥ 0) (INV-CPART-2/3); ActiveParticipantCount пересчитан. Media-стеку
отдана команда выселить окупанта из комнаты (kick); фактический eject подтверждает presence-вебхук
(jitsi-events.md, INV-CPART-5). Поднят ConferenceParticipantLeftEvent
(RemovedByModerator) → в той же транзакции записано намерение доставки (persist-then-push): после коммита
realtime-dispatch пушит MediaParticipant(Left) в chat:{ChatId}. Kafka-события удаление не публикует.
Алгоритм
- Контекст и доступ. Операцию выполняет модератор (
User);ActorUserId— из JWT; требуется правоmessenger.conferences.use. Конференции включены флагомCONFERENCE_CALLS_ENABLED, иначе —MSG_CONFERENCES_DISABLED. - Проверка входа.
ConferenceIdиUserIdнепустые; иначе ошибка валидации. Тела запроса нет. - Загрузка сессии. Конференция по
ConferenceId; нет →MSG_CONFERENCE_NOT_FOUND. Требуется статусActive; иначеMSG_CONFERENCE_NOT_ACTIVE. - Право модератора. Актор — активный участник Deal-чата (INV-CHAT-4), иначе
MSG_CHAT_ACCESS_DENIED; его строка ростера —ConferenceRole = Moderator(INV-CONF-6), иначеMSG_CONFERENCE_FORBIDDEN(обычный участник удалять других не может). - Цель и переход. Строка
(ConferenceId, UserId)в статусеJoined; нет активной строки цели →MSG_CONFERENCE_PARTICIPANT_NOT_FOUND(цель не в комнате). Если строка ужеLeft— идемпотентный no-op (200, состояние не меняется, лишнего push/kick нет). При переходе —State = Left,LeftAt,LeaveReason = RemovedByModerator,DurationSeconds = LeftAt − JoinedAt(INV-CPART-2/3); пересчитываетсяActiveParticipantCount. Крайний случай самоудаления модератора (обычно — через leave-conference.md): если модератор снял себя и остаются активные — роль модератора передаётся старейшему активному (INV-CONF-6). - Выселение из комнаты. Media-стеку отдаётся команда kick окупанта
UserIdиз комнаты (внешний вызов); фактический eject подтверждает presence-вебхук media-стека (INV-CPART-5, ростер сходится к серверному событию). Недоступность media-стека не откатывает зафиксированный выход — окупант будет выселен по presence-сверке. - Присутствие. В том же согласованном изменении поднимается
ConferenceParticipantLeftEvent(RemovedByModerator) и фиксируется намерение pushMediaParticipant(Left)вchat:{ChatId}(persist-then-push); при передаче модерации — ещёConferenceSignal. Всё — одна транзакция (атомарно). - Результат. Актуальная карточка
ConferenceModel(безRoom) → HTTP200 OK.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none — удаление участника Kafka не публикует |
| Realtime push (hub) | MediaParticipant(Left) → chat:{ChatId}; при передаче модерации — ConferenceSignal (messenger-hub.md) |
| External calls | команда kick окупанта в media-стек (SFU, ADR-0062); подтверждение — presence-вебхук (jitsi-events.md) |
| Cache / projections / search | пересчёт ActiveParticipantCount; ростер (ix_call_participants_active) |
| Notifications | нет |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | ConferenceId/UserId = Empty | «Проверьте параметры запроса» | no retry |
MSG_CONFERENCE_NOT_FOUND | 404 | конференции нет | «Конференция не найдена» | no retry |
MSG_CONFERENCE_NOT_ACTIVE | 409 | конференция не Active | «Конференция не активна» | no retry (перечитать) |
MSG_CHAT_ACCESS_DENIED | 403 | актор не активный участник Deal-чата | «Нет доступа к конференции» | no retry |
MSG_CONFERENCE_FORBIDDEN | 403 | актор не модератор (ConferenceRole != Moderator) | «Недостаточно прав: нужен модератор» | no retry |
MSG_CONFERENCE_PARTICIPANT_NOT_FOUND | 404 | у цели нет активной (Joined) строки ростера | «Участник не найден в конференции» | no retry |
MSG_CONFERENCES_DISABLED | 503 | CONFERENCE_CALLS_ENABLED = false | «Конференции временно недоступны» | retry позже |
Acceptance Criteria
- Удаление участника (happy). Дано: актор — модератор
Active-конференции, цельJoined. КогдаPOST /participants/{userId}/remove. Тогда200, строка цели →Left(RemovedByModerator,DurationSeconds),ActiveParticipantCountуменьшен; media-стеку отдан kick; остальные получаютMediaParticipant(Left). - Не модератор. Дано: актор — обычный участник (
Speaker). Когда пытается удалить другого. Тогда403 MSG_CONFERENCE_FORBIDDEN, состояние не меняется. - Цель не в комнате. Дано: у цели нет активной (
Joined) строки (уже вышла / не входила). КогдаPOST /remove. Тогда404 MSG_CONFERENCE_PARTICIPANT_NOT_FOUND. - Идемпотентность. Дано: цель уже
Left(повтор удаления). КогдаPOST /remove. Тогда200no-op без ошибки, лишнего push/kick нет. - Конференция не активна. Дано: конференция
Ended/Provisioning/Failed. КогдаPOST /remove. Тогда409 MSG_CONFERENCE_NOT_ACTIVE. - Media-стек недоступен. Дано: SFU не принял команду kick. Когда
POST /remove. Тогда зафиксированный выход не откатывается (200), окупант выселяется по presence-сверке (INV-CPART-5); присутствие сходится к серверному ростеру. - Не участник чата. Дано: актор не активный участник Deal-чата. Когда
POST /remove. Тогда403 MSG_CHAT_ACCESS_DENIED. - Нет конференции. Дано:
ConferenceIdне существует. Тогда404 MSG_CONFERENCE_NOT_FOUND.
Совместимость и наблюдаемость
- Новые optional поля ответа — additive; смена маршрута — breaking. Kafka-контракта нет.
- Logs:
conference remove requested/removed/forbidden(ConferenceId,ActorUserId,TargetUserId,DurationSeconds); metrics:messenger_conference_remove_total{result}, latency p95; traces — сквозной trace (traceparent); dashboardpin-messenger.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: ConferenceSessionEntity — групповой звонок в чате сделки, Entity: CallParticipantEntity (owned-child ConferenceSessionEntity)
- API (2): POST /api/messenger/conferences/{conferenceId}/decline — Отклонить приглашение в конференцию, POST /api/messenger/conferences/{conferenceId}/invite — Пригласить участников в конференцию
- calls (1): Жизненный цикл сессии звонка — 1:1 и конференция (messenger)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей,
INV-CPART-2/3/5,RemovedByModerator,ConferenceParticipantLeftEvent): call-participant.md; конференция (INV-CONF-6): conference-session.md. - Событие
MediaParticipant/ConferenceSignal: messenger-hub.md; presence-вебхук (авторитет ростера, подтверждение eject): jitsi-events.md. - Соседние операции: invite-participant.md, join-conference.md, leave-conference.md, decline-conference.md, end-conference.md, get-conference.md; контекст: README.
- Разбор жизненного цикла: session-lifecycle.md.
- ADR: 0062 медиа-стек и запись, 0055 свой мини-мессенджер.