feat(dm): ручки и события звонков в беседах (Фаза 7)
Звонки 1:1 и в групповых беседах до 10 участников поверх той же голосовой
инфраструктуры LiveKit: те же токены, что у серверных комнат, но без
серверных ролей — позвонить может участник беседы.
Ручки: POST /channels/{id}/call (создание, в ответе токен и адрес
сигналинга), accept, decline, end, PATCH /call/@me для флагов микрофона,
камеры и шаринга, GET /call (текущий звонок) и GET /calls (история: кто
звонил, когда, сколько длился, кто пропустил). Постороннему — 404,
существование чужой беседы не подтверждаем; в беседе больше десяти
участников звонить нельзя (dm.call_limit); выключенный голос отвечает
voice.disabled.
События: DM_CALL_RING приходит приглашённым, DM_CALL_UPDATE — участникам
при смене состояния, DM_CALL_END — с итогом и причиной. Звонок добавлен в
READY и в карточку беседы (active_call), поэтому «идёт звонок» переживает
перезагрузку. Вебхук LiveKit разбирает комнаты dm_call_* и синхронизирует
состояние, сторож голосовых состояний закрывает гудки без ответа и
опустевшие комнаты.
Тесты: жизненный цикл 1:1, отклонение и пропущенный, отказ постороннему
на всех ручках, предел десяти участников, поведение при выключенном
голосе, события Gateway и active_call в READY.
This commit is contained in:
@@ -112,6 +112,11 @@ func (s *Snapshot) Build(ctx context.Context, user *store.User) (*Ready, error)
|
||||
// У 1:1 иконка беседы — аватар собеседника (AGENT.md 7.8).
|
||||
channel.IconFileID = formatID(*summary.AvatarFileID)
|
||||
}
|
||||
// Звонок в беседе: клиент рисует маркер «идёт звонок» сразу после
|
||||
// подключения, а не только по событию (AGENT.md 7.8).
|
||||
if call, err := s.store.GetLiveDMCall(ctx, summary.Channel.ID); err == nil {
|
||||
channel.ActiveCall = readyDMCall(call)
|
||||
}
|
||||
if summary.LastMessageID != 0 {
|
||||
channel.LastMessageID = formatID(summary.LastMessageID)
|
||||
}
|
||||
@@ -427,6 +432,34 @@ func optionalFormatID(id *uint64) string {
|
||||
return formatID(*id)
|
||||
}
|
||||
|
||||
// readyDMCall переводит звонок из хранилища в форму снапшота: та же форма, что
|
||||
// у событий DM_CALL_* и REST-ручек звонков (AGENT.md 8.3).
|
||||
func readyDMCall(call *store.DMCallWithParticipants) *ReadyDMCall {
|
||||
payload := &ReadyDMCall{
|
||||
ID: formatID(call.ID),
|
||||
ChannelID: formatID(call.ChannelID),
|
||||
InitiatorID: formatID(call.InitiatorID),
|
||||
Status: call.Status,
|
||||
Media: call.Media,
|
||||
CreatedAt: call.CreatedAt.UTC().Format(time.RFC3339),
|
||||
Participants: make([]ReadyDMCallParticipant, 0, len(call.Participants)),
|
||||
}
|
||||
if call.StartedAt != nil {
|
||||
payload.StartedAt = call.StartedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
for _, participant := range call.Participants {
|
||||
payload.Participants = append(payload.Participants, ReadyDMCallParticipant{
|
||||
UserID: formatID(participant.UserID),
|
||||
State: participant.State,
|
||||
SelfMute: participant.SelfMute,
|
||||
SelfDeaf: participant.SelfDeaf,
|
||||
Camera: participant.Camera,
|
||||
Screen: participant.Screen,
|
||||
})
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func formatID(id uint64) string {
|
||||
if id == 0 {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user