192 lines
5.1 KiB
Go
192 lines
5.1 KiB
Go
|
|
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=<base64 encoded signature>"
|
||
|
|
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
|
||
|
|
}
|