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:
@@ -78,6 +78,10 @@ type dmChannelPayload struct {
|
||||
MemberCount int `json:"member_count,omitempty"`
|
||||
OwnerID string `json:"owner_id,omitempty"`
|
||||
Recipients []dmRecipientPayload `json:"recipients,omitempty"`
|
||||
// ActiveCall — идущий в беседе звонок (AGENT.md 7.8, Фаза 7): список
|
||||
// бесед и шапка показывают маркер «идёт звонок» и после перезагрузки, не
|
||||
// дожидаясь события DM_CALL_UPDATE.
|
||||
ActiveCall *dmCallPayload `json:"active_call,omitempty"`
|
||||
}
|
||||
|
||||
// dmRecipientPayload — участник групповой беседы.
|
||||
@@ -404,7 +408,7 @@ func (s *Server) registerSocialRoutes(api huma.API) {
|
||||
Timezone: recipient.Timezone,
|
||||
}
|
||||
output := &dmChannelOutput{}
|
||||
output.Body.Channel = s.dmChannelPayload(summary)
|
||||
output.Body.Channel = s.dmChannelPayload(ctx, summary)
|
||||
return output, nil
|
||||
})
|
||||
|
||||
@@ -427,7 +431,7 @@ func (s *Server) registerSocialRoutes(api huma.API) {
|
||||
output := &dmChannelListOutput{}
|
||||
output.Body.Channels = make([]dmChannelPayload, 0, len(channels))
|
||||
for _, summary := range channels {
|
||||
output.Body.Channels = append(output.Body.Channels, s.dmChannelPayload(summary))
|
||||
output.Body.Channels = append(output.Body.Channels, s.dmChannelPayload(ctx, summary))
|
||||
}
|
||||
return output, nil
|
||||
})
|
||||
@@ -481,7 +485,7 @@ func (s *Server) registerSocialRoutes(api huma.API) {
|
||||
return nil, err
|
||||
}
|
||||
output := &dmChannelOutput{}
|
||||
output.Body.Channel = s.dmChannelPayload(summary)
|
||||
output.Body.Channel = s.dmChannelPayload(ctx, summary)
|
||||
return output, nil
|
||||
})
|
||||
|
||||
@@ -544,7 +548,7 @@ func (s *Server) registerSocialRoutes(api huma.API) {
|
||||
return nil, err
|
||||
}
|
||||
output := &dmChannelOutput{}
|
||||
output.Body.Channel = s.dmChannelPayload(summary)
|
||||
output.Body.Channel = s.dmChannelPayload(ctx, summary)
|
||||
return output, nil
|
||||
})
|
||||
|
||||
@@ -639,7 +643,7 @@ func (s *Server) registerSocialRoutes(api huma.API) {
|
||||
return nil, err
|
||||
}
|
||||
output := &dmChannelOutput{}
|
||||
output.Body.Channel = s.dmChannelPayload(summary)
|
||||
output.Body.Channel = s.dmChannelPayload(ctx, summary)
|
||||
return output, nil
|
||||
})
|
||||
}
|
||||
@@ -926,12 +930,15 @@ func (s *Server) relationshipPayload(_ uint64, profile *store.RelationshipProfil
|
||||
}
|
||||
|
||||
// dmChannelPayload собирает личную беседу для API.
|
||||
func (s *Server) dmChannelPayload(summary store.DMChannelSummary) dmChannelPayload {
|
||||
func (s *Server) dmChannelPayload(ctx context.Context, summary store.DMChannelSummary) dmChannelPayload {
|
||||
payload := dmChannelPayload{
|
||||
ID: formatSnowflake(summary.Channel.ID),
|
||||
Type: string(store.ChannelDM),
|
||||
CanSend: true,
|
||||
CanView: true,
|
||||
// Звонок приходит вместе с беседой: клиенту не нужен отдельный запрос,
|
||||
// чтобы показать «идёт звонок» в списке и шапке (AGENT.md 7.8).
|
||||
ActiveCall: s.liveCallPayload(ctx, summary.Channel.ID),
|
||||
}
|
||||
if summary.IsGroup {
|
||||
payload.IsGroup = true
|
||||
|
||||
Reference in New Issue
Block a user