- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion. - Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects. - Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management. - Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering. - Improve database initialization by moving it from `internal/model` to `internal/database`. - Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool. - Enhance error handling and security by implementing fail-fast checks for encryption key length during startup. - Clean up unused code, including the `avatar` package and several unused DTOs.
128 lines
3.4 KiB
Go
128 lines
3.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"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"
|
|
)
|
|
|
|
// RoomConfig 房间配置选项
|
|
type RoomConfig struct {
|
|
MaxParticipants uint32
|
|
EmptyTimeout uint32 // 秒
|
|
}
|
|
|
|
// LiveKitService LiveKit 令牌生成和 Webhook 处理服务
|
|
type LiveKitService interface {
|
|
GenerateToken(roomName, userID string, canPublish, canSubscribe bool, roomCfg ...RoomConfig) (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, roomCfg ...RoomConfig) (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: &canPublish,
|
|
}
|
|
|
|
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
|
|
}
|