Introduces group calling capabilities, including group invites and participant lifecycle management. Enhances the existing call system with support for media types (voice/video) and improved participant tracking. - Add `GroupInvite` and `ParticipantJoin/Leave` to `CallService`. - Implement WebSocket handlers for `call_group_invite` and `call_participant_join`. - Update `CallSession` model to support group IDs, media types, and tracking who ended the call. - Integrate `GroupService` into `CallService` via dependency injection. - Add error constants for group call limitations and participant capacity. - Update LiveKit webhook handling to process participant leave events. Refactor grade synchronization to remove semester-based filtering in favor of a more flexible user-based approach and improve GPA summary updates.
166 lines
4.3 KiB
Go
166 lines
4.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/livekit/protocol/livekit"
|
|
"go.uber.org/zap"
|
|
|
|
"with_you/internal/config"
|
|
"with_you/internal/pkg/response"
|
|
"with_you/internal/service"
|
|
)
|
|
|
|
// LiveKitHandler LiveKit 令牌和 Webhook 处理器
|
|
type LiveKitHandler struct {
|
|
liveKitService service.LiveKitService
|
|
callService service.CallService
|
|
config *config.LiveKitConfig
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewLiveKitHandler 创建 LiveKit 处理器
|
|
func NewLiveKitHandler(
|
|
liveKitService service.LiveKitService,
|
|
callService service.CallService,
|
|
cfg *config.Config,
|
|
logger *zap.Logger,
|
|
) *LiveKitHandler {
|
|
return &LiveKitHandler{
|
|
liveKitService: liveKitService,
|
|
callService: callService,
|
|
config: &cfg.LiveKit,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// GetToken 生成 LiveKit 访问令牌
|
|
// GET /api/v1/calls/token?room=<callID>
|
|
func (h *LiveKitHandler) GetToken(c *gin.Context) {
|
|
if !h.config.Enabled {
|
|
response.Forbidden(c, "livekit is not enabled")
|
|
return
|
|
}
|
|
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
response.Unauthorized(c, "unauthorized")
|
|
return
|
|
}
|
|
|
|
room := c.Query("room")
|
|
if room == "" {
|
|
response.BadRequest(c, "room parameter is required")
|
|
return
|
|
}
|
|
|
|
// 验证用户是该通话的参与者
|
|
activeCall := h.callService.GetActiveCall(c.Request.Context(), room)
|
|
if activeCall == nil {
|
|
response.NotFound(c, "call not found or already ended")
|
|
return
|
|
}
|
|
|
|
isParticipant := false
|
|
for _, p := range activeCall.Participants {
|
|
if p.UserID == userID.(string) {
|
|
isParticipant = true
|
|
break
|
|
}
|
|
}
|
|
if !isParticipant && activeCall.CallerID != userID.(string) && activeCall.CalleeID != userID.(string) {
|
|
response.Forbidden(c, "not a participant of this call")
|
|
return
|
|
}
|
|
|
|
canPublish := true
|
|
canSubscribe := true
|
|
if cp := c.Query("can_publish"); cp != "" {
|
|
canPublish, _ = strconv.ParseBool(cp)
|
|
}
|
|
if cs := c.Query("can_subscribe"); cs != "" {
|
|
canSubscribe, _ = strconv.ParseBool(cs)
|
|
}
|
|
|
|
token, err := h.liveKitService.GenerateToken(room, userID.(string), canPublish, canSubscribe)
|
|
if err != nil {
|
|
h.logger.Error("failed to generate livekit token", zap.Error(err))
|
|
response.InternalServerError(c, "failed to generate token")
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"token": token,
|
|
"url": h.config.URL,
|
|
})
|
|
}
|
|
|
|
// HandleWebhook 处理 LiveKit Webhook 回调
|
|
// POST /api/v1/calls/webhook
|
|
func (h *LiveKitHandler) HandleWebhook(c *gin.Context) {
|
|
if !h.config.Enabled {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "livekit is not enabled"})
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
h.logger.Error("failed to read webhook body", zap.Error(err))
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read body"})
|
|
return
|
|
}
|
|
|
|
authHeader := c.GetHeader("Authorization")
|
|
event, err := h.liveKitService.ProcessWebhook(c.Request.Context(), body, authHeader)
|
|
if err != nil {
|
|
h.logger.Error("failed to process webhook", zap.Error(err))
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid webhook"})
|
|
return
|
|
}
|
|
|
|
// 根据 webhook 事件类型处理
|
|
switch event.Event {
|
|
case "room_finished":
|
|
h.handleRoomFinished(c.Request.Context(), event)
|
|
case "participant_left":
|
|
identity := event.Participant.GetIdentity()
|
|
roomName := event.Room.GetName()
|
|
h.logger.Info("participant left room",
|
|
zap.String("room", roomName),
|
|
zap.String("identity", identity),
|
|
)
|
|
if identity != "" && roomName != "" {
|
|
if err := h.callService.ParticipantLeave(c.Request.Context(), roomName, identity, "left_livekit"); err != nil {
|
|
h.logger.Warn("failed to handle participant_left",
|
|
zap.String("room", roomName),
|
|
zap.String("identity", identity),
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// handleRoomFinished 处理 room_finished 事件,作为通话历史持久化的安全兜底
|
|
func (h *LiveKitHandler) handleRoomFinished(ctx context.Context, event *livekit.WebhookEvent) {
|
|
roomName := event.Room.GetName()
|
|
if roomName == "" {
|
|
return
|
|
}
|
|
|
|
// 尝试结束通话(如果尚未结束)
|
|
// callService.End 会检查调用是否仍然活跃,如果已经结束则返回 nil
|
|
_, err := h.callService.End(ctx, roomName, "", "room_finished")
|
|
if err != nil {
|
|
h.logger.Debug("room_finished: call end returned error (may already be ended)",
|
|
zap.String("room", roomName),
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
} |