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= 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), ) } }