package service import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "time" "with_you/internal/config" "github.com/livekit/protocol/auth" "github.com/livekit/protocol/livekit" "go.uber.org/zap" "google.golang.org/protobuf/encoding/protojson" ) // LiveKitService LiveKit 令牌生成和 Webhook 处理服务 type LiveKitService interface { GenerateToken(roomName, userID string, canPublish, canSubscribe bool) (string, error) ProcessWebhook(ctx context.Context, body []byte, authHeader string) (*livekit.WebhookEvent, error) } type liveKitService struct { config *config.LiveKitConfig logger *zap.Logger } // NewLiveKitService 创建 LiveKit 服务 func NewLiveKitService(cfg *config.Config, logger *zap.Logger) LiveKitService { return &liveKitService{ config: &cfg.LiveKit, logger: logger, } } // GenerateToken 为指定 room 和 user 生成 LiveKit 访问令牌 func (s *liveKitService) GenerateToken(roomName, userID string, canPublish, canSubscribe bool) (string, error) { if !s.config.Enabled { return "", fmt.Errorf("livekit is not enabled") } ttl := s.config.TokenTTL if ttl <= 0 { ttl = 600 // 默认 10 分钟 } at := auth.NewAccessToken(s.config.APIKey, s.config.APISecret) at.SetName(userID) at.SetIdentity(userID) at.SetValidFor(time.Duration(ttl) * time.Second) grant := &auth.VideoGrant{ RoomJoin: true, Room: roomName, CanPublish: &canPublish, CanSubscribe: &canSubscribe, CanPublishData: &canSubscribe, } at.SetVideoGrant(grant) token, err := at.ToJWT() if err != nil { s.logger.Error("failed to generate livekit token", zap.Error(err)) return "", fmt.Errorf("generate token: %w", err) } return token, nil } // ProcessWebhook 验证并解析 LiveKit Webhook 事件 func (s *liveKitService) ProcessWebhook(ctx context.Context, body []byte, authHeader string) (*livekit.WebhookEvent, error) { if s.config.WebhookSecret != "" { if err := s.verifyWebhookSignature(body, authHeader); err != nil { return nil, fmt.Errorf("webhook signature verification failed: %w", err) } } var event livekit.WebhookEvent if err := protojson.Unmarshal(body, &event); err != nil { return nil, fmt.Errorf("failed to unmarshal webhook event: %w", err) } s.logger.Info("received livekit webhook", zap.String("event", event.Event), zap.String("room_name", event.Room.GetName()), ) return &event, nil } // verifyWebhookSignature 验证 Webhook 签名 func (s *liveKitService) verifyWebhookSignature(body []byte, authHeader string) error { // LiveKit webhook 使用 SHA256 HMAC 签名 // Authorization header 格式: "sha256=" if authHeader == "" { return fmt.Errorf("missing authorization header") } expectedPrefix := "sha256=" if len(authHeader) < len(expectedPrefix) || authHeader[:len(expectedPrefix)] != expectedPrefix { return fmt.Errorf("invalid authorization header format") } expectedSig, err := base64.StdEncoding.DecodeString(authHeader[len(expectedPrefix):]) if err != nil { return fmt.Errorf("failed to decode signature: %w", err) } mac := hmac.New(sha256.New, []byte(s.config.WebhookSecret)) mac.Write(body) actualSig := mac.Sum(nil) if !hmac.Equal(actualSig, expectedSig) { return fmt.Errorf("signature mismatch") } return nil } // WebhookRoomFinished 处理 room_finished 事件的负载 type WebhookRoomFinished struct { RoomName string `json:"room_name"` Duration int64 `json:"duration"` StartedAt int64 `json:"started_at"` EndedAt int64 `json:"ended_at"` } // ParseRoomFinished 从 webhook event 中解析 room_finished 数据 func ParseRoomFinished(event *livekit.WebhookEvent) (*WebhookRoomFinished, error) { if event.Event != "room_finished" { return nil, fmt.Errorf("not a room_finished event: %s", event.Event) } room := event.Room if room == nil { return nil, fmt.Errorf("room is nil in webhook event") } duration := int64(0) startedAt := int64(0) endedAt := int64(0) if room.CreationTime > 0 { startedAt = room.CreationTime } endedAt = time.Now().Unix() if startedAt > 0 { duration = endedAt - startedAt } return &WebhookRoomFinished{ RoomName: room.Name, Duration: duration, StartedAt: startedAt, EndedAt: endedAt, }, nil } // WebhookEventJSON 用于 JSON 序列化的 webhook 事件 type WebhookEventJSON struct { Event string `json:"event"` Room *WebhookRoomJSON `json:"room,omitempty"` RoomName string `json:"room_name,omitempty"` } // WebhookRoomJSON webhook 房间信息 JSON type WebhookRoomJSON struct { Sid string `json:"sid,omitempty"` Name string `json:"name,omitempty"` CreationTime int64 `json:"creation_time,omitempty"` NumParticipants uint32 `json:"num_participants,omitempty"` } // WebhookEventToJSON 将 webhook event 转为自定义 JSON 结构(避免 protobuf 内部字段) func WebhookEventToJSON(event *livekit.WebhookEvent) (*WebhookEventJSON, error) { data, err := protojson.Marshal(event) if err != nil { return nil, err } var result WebhookEventJSON if err := json.Unmarshal(data, &result); err != nil { return nil, err } return &result, nil }