feat(call): integrate LiveKit for voice and video calling
Replace the manual WebRTC signaling implementation with LiveKit SFU. This includes: - Adding LiveKit service, handler, and configuration. - Updating Docker Compose to include LiveKit server, Redis, and PostgreSQL. - Refactoring `CallService` and `WSHandler` to support LiveKit room readiness instead of raw SDP/ICE relaying. - Adding new API endpoints for LiveKit token generation and webhooks. - Removing deprecated WebRTC configuration and manual signaling DTOs.
This commit is contained in:
@@ -63,6 +63,7 @@ func ProvideRouter(
|
||||
activityService service.UserActivityService,
|
||||
casbinService service.CasbinService,
|
||||
wsHandler *handler.WSHandler,
|
||||
liveKitHandler *handler.LiveKitHandler,
|
||||
) *router.Router {
|
||||
return router.New(router.RouterDeps{
|
||||
UserRepo: userRepo,
|
||||
@@ -92,6 +93,7 @@ func ProvideRouter(
|
||||
QRCodeHandler: qrcodeHandler,
|
||||
MaterialHandler: materialHandler,
|
||||
CallHandler: callHandler,
|
||||
LiveKitHandler: liveKitHandler,
|
||||
ReportHandler: reportHandler,
|
||||
AdminReportHandler: adminReportHandler,
|
||||
VerificationHandler: verificationHandler,
|
||||
|
||||
@@ -333,3 +333,15 @@ version_log:
|
||||
enabled: false # 启用后客户端可使用 /conversations/sync 增量同步
|
||||
sync_limit: 100 # 单次同步最大返回变更条数
|
||||
max_sync_gap: 1000 # 版本差距超过此值建议全量同步
|
||||
|
||||
# LiveKit SFU 配置(语音/视频通话)
|
||||
# 环境变量:
|
||||
# APP_LIVEKIT_ENABLED, APP_LIVEKIT_URL, APP_LIVEKIT_API_KEY, APP_LIVEKIT_API_SECRET
|
||||
# APP_LIVEKIT_TOKEN_TTL, APP_LIVEKIT_WEBHOOK_SECRET
|
||||
livekit:
|
||||
enabled: false
|
||||
url: "http://localhost:7880"
|
||||
api_key: "devkey"
|
||||
api_secret: "u1sLYiHWe4HIr3KrJrvknIleoHdgHdDqSjfkBvRs2AlA"
|
||||
token_ttl: 600 # token 有效期(秒)
|
||||
webhook_secret: "" # webhook 签名密钥(与 LiveKit 服务端配置一致)
|
||||
|
||||
32
configs/livekit.yaml
Normal file
32
configs/livekit.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# LiveKit Server Configuration
|
||||
# See https://docs.livekit.io/realtime/server/config/ for all options
|
||||
|
||||
port: 7880
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
udp_port: 7882
|
||||
use_external_ip: false
|
||||
# Enable TURN/TLS relay for restrictive networks
|
||||
port_range_start: 7881
|
||||
port_range_end: 7882
|
||||
node_ip: 127.0.0.1
|
||||
|
||||
keys:
|
||||
devkey: u1sLYiHWe4HIr3KrJrvknIleoHdgHdDqSjfkBvRs2AlA
|
||||
|
||||
logging:
|
||||
level: info
|
||||
|
||||
# Redis for distributed state (required)
|
||||
redis:
|
||||
address: livekit-redis:6379
|
||||
|
||||
# PostgreSQL for persistent storage (rooms, participants, egress)
|
||||
db:
|
||||
address: postgres://postgres:postgres@livekit-postgres:5432/livekit?sslmode=disable
|
||||
|
||||
# Webhook for call history persistence
|
||||
# webhook:
|
||||
# urls:
|
||||
# - http://app:8080/api/v1/calls/webhook
|
||||
# api_key: devkey
|
||||
@@ -55,6 +55,58 @@ services:
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
# LiveKit SFU Server (voice/video calling)
|
||||
livekit:
|
||||
image: livekit/livekit-server:v1.9.5
|
||||
container_name: livekit
|
||||
ports:
|
||||
- "7880:7880" # HTTP (token, webhooks, dashboard)
|
||||
- "7881:7881" # TCP/TURN fallback
|
||||
- "7882:7882" # UDP media
|
||||
volumes:
|
||||
- ./configs/livekit.yaml:/etc/livekit.yaml
|
||||
command: --config /etc/livekit.yaml --node-ip=127.0.0.1
|
||||
depends_on:
|
||||
livekit-redis:
|
||||
condition: service_started
|
||||
livekit-postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# LiveKit Redis (required for distributed coordination)
|
||||
livekit-redis:
|
||||
image: redis:7-alpine
|
||||
container_name: livekit_redis
|
||||
ports:
|
||||
- "6380:6379"
|
||||
volumes:
|
||||
- livekit_redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
# LiveKit PostgreSQL (required for room/participant state)
|
||||
livekit-postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: livekit_postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: livekit
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- livekit_pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d livekit"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
@@ -76,3 +128,5 @@ volumes:
|
||||
# postgres_data:
|
||||
# redis_data:
|
||||
minio_data:
|
||||
livekit_redis_data:
|
||||
livekit_pg_data:
|
||||
|
||||
48
go.mod
48
go.mod
@@ -13,6 +13,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/livekit/protocol v1.39.4-0.20250721114233-52633eee694f
|
||||
github.com/minio/minio-go/v7 v7.0.99
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
github.com/spf13/viper v1.21.0
|
||||
@@ -25,24 +26,36 @@ require (
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
gorm.io/plugin/dbresolver v1.6.2
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1 // indirect
|
||||
buf.build/go/protovalidate v0.14.0 // indirect
|
||||
buf.build/go/protoyaml v0.6.0 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/casbin/govaluate v1.10.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dennwc/iters v1.1.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/frostbyte73/core v0.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gammazero/deque v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
@@ -52,6 +65,7 @@ require (
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.1 // indirect
|
||||
@@ -59,10 +73,14 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/jxskiss/base62 v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0 // indirect
|
||||
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 // indirect
|
||||
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.37 // indirect
|
||||
github.com/microsoft/go-mssqldb v1.9.8 // indirect
|
||||
@@ -70,10 +88,31 @@ require (
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nats-io/nats.go v1.43.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/opencontainers/runc v1.1.14 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.21 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.15 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.6 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pion/webrtc/v4 v4.1.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
@@ -84,28 +123,35 @@ require (
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/mysql v1.6.0 // indirect
|
||||
gorm.io/driver/sqlserver v1.6.3 // indirect
|
||||
gorm.io/plugin/dbresolver v1.6.2 // indirect
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
||||
138
go.sum
138
go.sum
@@ -1,3 +1,13 @@
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1 h1:Lg6klmCi3v7VvpqeeLEER9/m5S8y9e9DjhqQnSCNy4k=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
|
||||
buf.build/go/protovalidate v0.14.0 h1:kr/rC/no+DtRyYX+8KXLDxNnI1rINz0imk5K44ZpZ3A=
|
||||
buf.build/go/protovalidate v0.14.0/go.mod h1:+F/oISho9MO7gJQNYC2VWLzcO1fTPmaTA08SDYJZncA=
|
||||
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
|
||||
buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||
@@ -20,12 +30,22 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
|
||||
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
@@ -46,28 +66,46 @@ github.com/casbin/gorm-adapter/v3 v3.41.0/go.mod h1:BQZRJhwUnwMpI+pT2m7/cUJwXxrH
|
||||
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
|
||||
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
|
||||
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dennwc/iters v1.1.0 h1:PsS3DbOU7GxSUQO0e7SGmzHkPhtwOlwbqggJ++Bgnr8=
|
||||
github.com/dennwc/iters v1.1.0/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
|
||||
github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/frostbyte73/core v0.1.1 h1:ChhJOR7bAKOCPbA+lqDLE2cGKlCG5JXsDvvQr4YaJIA=
|
||||
github.com/frostbyte73/core v0.1.1/go.mod h1:mhfOtR+xWAvwXiwor7jnqPMnu4fxbv1F2MwZ0BEpzZo=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo=
|
||||
github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
|
||||
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
@@ -78,6 +116,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
@@ -98,6 +138,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
@@ -109,14 +151,17 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -150,6 +195,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
|
||||
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
@@ -170,6 +217,14 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
|
||||
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5ATTo469PQPkqzdoU7be46ryiCDO3boc=
|
||||
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
|
||||
github.com/livekit/protocol v1.39.4-0.20250721114233-52633eee694f h1:Cwe38+/ld3r5dnNmIZSALSoZPWNEMeYPZIi/qjpplLo=
|
||||
github.com/livekit/protocol v1.39.4-0.20250721114233-52633eee694f/go.mod h1:YlgUxAegtU8jZ0tVXoIV/4fHeHqqLvS+6JnPKDbpFPU=
|
||||
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c h1:WwEr0YBejYbKzk8LSaO9h8h0G9MnE7shyDu8yXQWmEc=
|
||||
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c/go.mod h1:kmD+AZPkWu0MaXIMv57jhNlbiSZZ/Jx4bzlxBDVmJes=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
|
||||
@@ -183,6 +238,12 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE=
|
||||
github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -190,12 +251,58 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/nats-io/nats.go v1.43.0 h1:uRFZ2FEoRvP64+UUhaTokyS18XBCR/xM2vQZKO4i8ug=
|
||||
github.com/nats-io/nats.go v1.43.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w=
|
||||
github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA=
|
||||
github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
|
||||
github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.21 h1:3yrOwmZFyUpcIosNcWRpQaU+UXIJ6yxLuJ8Bx0mw37Y=
|
||||
github.com/pion/rtp v1.8.21/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.15 h1:F0I1zds+K/+37ZrzdADmx2Q44OFDOPRLhPnNTaUX9hk=
|
||||
github.com/pion/sdp/v3 v3.0.15/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
|
||||
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
|
||||
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
@@ -204,6 +311,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
@@ -220,8 +329,12 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
|
||||
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
@@ -230,6 +343,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -249,13 +364,25 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
@@ -282,6 +409,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||
@@ -300,6 +429,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||
@@ -401,6 +532,8 @@ golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
@@ -417,6 +550,7 @@ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkp
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -27,7 +27,7 @@ type Config struct {
|
||||
Casbin CasbinConfig `mapstructure:"casbin"`
|
||||
Encryption EncryptionConfig `mapstructure:"encryption"`
|
||||
HotRank HotRankConfig `mapstructure:"hot_rank"`
|
||||
WebRTC WebRTCConfig `mapstructure:"webrtc"`
|
||||
LiveKit LiveKitConfig `mapstructure:"livekit"`
|
||||
Report ReportConfig `mapstructure:"report"`
|
||||
Sensitive SensitiveConfig `mapstructure:"sensitive"`
|
||||
JPush JPushConfig `mapstructure:"jpush"`
|
||||
@@ -168,10 +168,6 @@ func Load(configPath string) (*Config, error) {
|
||||
viper.SetDefault("hot_rank.recent_window_hours", 168)
|
||||
viper.SetDefault("hot_rank.recent_fetch_limit", 500)
|
||||
viper.SetDefault("hot_rank.candidate_cap", 800)
|
||||
// WebRTC 默认值(Google 公共 STUN 服务器)
|
||||
viper.SetDefault("webrtc.ice_servers", []map[string]any{
|
||||
{"urls": []string{"stun:stun.l.google.com:19302"}},
|
||||
})
|
||||
// Report 默认值
|
||||
viper.SetDefault("report.auto_hide_threshold", 3)
|
||||
viper.SetDefault("jpush.enabled", false)
|
||||
@@ -213,6 +209,13 @@ func Load(configPath string) (*Config, error) {
|
||||
viper.SetDefault("push_worker.max_retries", 3)
|
||||
viper.SetDefault("push_worker.max_stream_len", 100000)
|
||||
viper.SetDefault("push_worker.idle_timeout_ms", 30000)
|
||||
// LiveKit 默认值
|
||||
viper.SetDefault("livekit.enabled", false)
|
||||
viper.SetDefault("livekit.url", "http://localhost:7880")
|
||||
viper.SetDefault("livekit.api_key", "devkey")
|
||||
viper.SetDefault("livekit.api_secret", "")
|
||||
viper.SetDefault("livekit.token_ttl", 600)
|
||||
viper.SetDefault("livekit.webhook_secret", "")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
|
||||
11
internal/config/livekit.go
Normal file
11
internal/config/livekit.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package config
|
||||
|
||||
// LiveKitConfig LiveKit SFU 配置
|
||||
type LiveKitConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
URL string `mapstructure:"url"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
APISecret string `mapstructure:"api_secret"`
|
||||
TokenTTL int `mapstructure:"token_ttl"`
|
||||
WebhookSecret string `mapstructure:"webhook_secret"`
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package config
|
||||
|
||||
// WebRTCConfig WebRTC ICE 服务器配置
|
||||
type WebRTCConfig struct {
|
||||
ICEServers []ICEServerConfig `mapstructure:"ice_servers"`
|
||||
}
|
||||
|
||||
// ICEServerConfig ICE 服务器配置
|
||||
type ICEServerConfig struct {
|
||||
URLs []string `mapstructure:"urls"`
|
||||
Username string `mapstructure:"username"`
|
||||
Credential string `mapstructure:"credential"`
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"with_you/internal/model"
|
||||
)
|
||||
|
||||
// ==================== Call Request DTOs ====================
|
||||
|
||||
// StartCallRequest 发起通话请求
|
||||
type StartCallRequest struct {
|
||||
ConversationID string `json:"conversation_id" binding:"required"` // 会话ID
|
||||
CallType model.CallType `json:"call_type" binding:"required"` // 通话类型: private, group
|
||||
}
|
||||
|
||||
// AnswerCallRequest 接听通话请求
|
||||
type AnswerCallRequest struct {
|
||||
SDP string `json:"sdp" binding:"required"` // SDP Answer
|
||||
}
|
||||
|
||||
// SendSDPRequest 发送SDP请求
|
||||
type SendSDPRequest struct {
|
||||
SDP string `json:"sdp" binding:"required"` // SDP Offer/Answer
|
||||
Type string `json:"type" binding:"required"` // offer 或 answer
|
||||
}
|
||||
|
||||
// SendICECandidateRequest 发送ICE候选请求
|
||||
type SendICECandidateRequest struct {
|
||||
Candidate string `json:"candidate" binding:"required"` // candidate 字符串
|
||||
SDPMid string `json:"sdp_mid"` // SDP mid
|
||||
SDPMLineIndex int16 `json:"sdp_mline_index"` // SDP m-line index
|
||||
}
|
||||
|
||||
// ==================== Call Response DTOs ====================
|
||||
|
||||
// CallSessionResponse 通话会话响应
|
||||
type CallSessionResponse struct {
|
||||
ID string `json:"id"` // 通话ID
|
||||
ConversationID string `json:"conversation_id"` // 会话ID
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
CallerID string `json:"caller_id"` // 发起者ID
|
||||
CallType model.CallType `json:"call_type"` // 通话类型
|
||||
Status model.CallStatus `json:"status"` // 通话状态
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
EndedAt *time.Time `json:"ended_at,omitempty"`
|
||||
Duration int64 `json:"duration"` // 通话时长(秒)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Participants []CallParticipantResponse `json:"participants"` // 参与者列表
|
||||
Caller *UserResponse `json:"caller,omitempty"` // 发起者信息
|
||||
}
|
||||
|
||||
// CallParticipantResponse 通话参与者响应
|
||||
type CallParticipantResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
Status model.ParticipantStatus `json:"status"`
|
||||
JoinedAt *time.Time `json:"joined_at,omitempty"`
|
||||
LeftAt *time.Time `json:"left_at,omitempty"`
|
||||
User *UserResponse `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// ==================== Call SSE Event DTOs ====================
|
||||
|
||||
// CallInviteEvent 来电邀请事件
|
||||
type CallInviteEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
CallerID string `json:"caller_id"`
|
||||
CallType model.CallType `json:"call_type"`
|
||||
Caller *UserResponse `json:"caller,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// CallAnswerEvent 接听响应事件
|
||||
type CallAnswerEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
SDP string `json:"sdp"` // SDP Answer
|
||||
}
|
||||
|
||||
// CallRejectEvent 拒绝通话事件
|
||||
type CallRejectEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// CallEndEvent 结束通话事件
|
||||
type CallEndEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Duration int64 `json:"duration"`
|
||||
}
|
||||
|
||||
// CallICECandidateEvent ICE候选事件
|
||||
type CallICECandidateEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Candidate string `json:"candidate"`
|
||||
SDPMid string `json:"sdp_mid"`
|
||||
SDPMLineIndex int16 `json:"sdp_mline_index"`
|
||||
}
|
||||
|
||||
// CallSDPOfferEvent SDP Offer事件
|
||||
type CallSDPOfferEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
SDP string `json:"sdp"`
|
||||
}
|
||||
|
||||
// CallSDPAnswerEvent SDP Answer事件
|
||||
type CallSDPAnswerEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
SDP string `json:"sdp"`
|
||||
}
|
||||
|
||||
// CallUserJoinedEvent 用户加入通话事件(群聊)
|
||||
type CallUserJoinedEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
User *UserResponse `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// CallUserLeftEvent 用户离开通话事件(群聊)
|
||||
type CallUserLeftEvent struct {
|
||||
CallID string `json:"call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// ==================== ICE Server Config ====================
|
||||
|
||||
// ICEServerConfig ICE服务器配置
|
||||
type ICEServerConfig struct {
|
||||
URLs []string `json:"urls"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
CredentialType string `json:"credential_type,omitempty"`
|
||||
}
|
||||
|
||||
// CallConfigResponse 通话配置响应
|
||||
type CallConfigResponse struct {
|
||||
ICEServers []ICEServerConfig `json:"ice_servers"`
|
||||
}
|
||||
|
||||
@@ -49,12 +49,3 @@ func (h *CallHandler) GetCallHistory(c *gin.Context) {
|
||||
"page": page,
|
||||
})
|
||||
}
|
||||
|
||||
// GetICEServers 获取 ICE 服务器配置
|
||||
// GET /api/v1/calls/ice-servers
|
||||
func (h *CallHandler) GetICEServers(c *gin.Context) {
|
||||
servers := h.callService.GetICEServers()
|
||||
response.Success(c, gin.H{
|
||||
"ice_servers": servers,
|
||||
})
|
||||
}
|
||||
|
||||
155
internal/handler/livekit_handler.go
Normal file
155
internal/handler/livekit_handler.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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":
|
||||
h.logger.Info("participant left room",
|
||||
zap.String("room", event.Room.GetName()),
|
||||
zap.String("identity", event.Participant.GetIdentity()),
|
||||
)
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ func (h *WSHandler) readPump(conn *websocket.Conn, client *ws.Client) {
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
conn.SetReadLimit(256 * 1024) // 256KB (SDP/ICE candidates can be large)
|
||||
conn.SetReadLimit(256 * 1024) // 256KB
|
||||
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
zap.L().Debug("WebSocket pong received",
|
||||
@@ -383,10 +383,8 @@ func (h *WSHandler) handleMessage(client *ws.Client, msg *ws.Message) {
|
||||
h.handleCallReject(ctx, client, msg.Payload)
|
||||
case "call_busy":
|
||||
h.handleCallBusy(ctx, client, msg.Payload)
|
||||
case "call_sdp":
|
||||
h.handleCallSDP(ctx, client, msg.Payload)
|
||||
case "call_ice":
|
||||
h.handleCallICE(ctx, client, msg.Payload)
|
||||
case "call_ready":
|
||||
h.handleCallReady(ctx, client, msg.Payload)
|
||||
case "call_end":
|
||||
h.handleCallEnd(ctx, client, msg.Payload)
|
||||
case "call_mute":
|
||||
@@ -717,43 +715,18 @@ func (h *WSHandler) handleCallBusy(ctx context.Context, client *ws.Client, paylo
|
||||
}
|
||||
}
|
||||
|
||||
// handleCallSDP 转发 SDP
|
||||
func (h *WSHandler) handleCallSDP(ctx context.Context, client *ws.Client, payload json.RawMessage) {
|
||||
// handleCallReady 处理通话就绪(LiveKit 房间连接成功)
|
||||
func (h *WSHandler) handleCallReady(ctx context.Context, client *ws.Client, payload json.RawMessage) {
|
||||
var req struct {
|
||||
CallID string `json:"call_id"`
|
||||
SDPType string `json:"sdp_type"`
|
||||
SDP json.RawMessage `json:"sdp"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" || req.SDPType == "" {
|
||||
h.publisher.SendError(client, "invalid_params", "call_id and sdp_type are required")
|
||||
return
|
||||
}
|
||||
|
||||
signalPayload, _ := json.Marshal(map[string]any{
|
||||
"sdp_type": req.SDPType,
|
||||
"sdp": json.RawMessage(req.SDP),
|
||||
})
|
||||
if err := h.callService.RelaySignal(ctx, req.CallID, client.UserID, "call_sdp", signalPayload); err != nil {
|
||||
h.publisher.SendError(client, "call_sdp_failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// handleCallICE 转发 ICE candidate
|
||||
func (h *WSHandler) handleCallICE(ctx context.Context, client *ws.Client, payload json.RawMessage) {
|
||||
var req struct {
|
||||
CallID string `json:"call_id"`
|
||||
Candidate json.RawMessage `json:"candidate"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
|
||||
h.publisher.SendError(client, "invalid_params", "call_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
signalPayload, _ := json.Marshal(map[string]any{
|
||||
"candidate": json.RawMessage(req.Candidate),
|
||||
})
|
||||
if err := h.callService.RelaySignal(ctx, req.CallID, client.UserID, "call_ice", signalPayload); err != nil {
|
||||
h.publisher.SendError(client, "call_ice_failed", err.Error())
|
||||
if err := h.callService.Ready(ctx, req.CallID, client.UserID); err != nil {
|
||||
h.publisher.SendError(client, "call_ready_failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuditTargetType 审核对象类型
|
||||
type AuditTargetType string
|
||||
|
||||
const (
|
||||
AuditTargetTypePost AuditTargetType = "post" // 帖子
|
||||
AuditTargetTypeComment AuditTargetType = "comment" // 评论
|
||||
AuditTargetTypeMessage AuditTargetType = "message" // 私信
|
||||
AuditTargetTypeUser AuditTargetType = "user" // 用户资料
|
||||
AuditTargetTypeImage AuditTargetType = "image" // 图片
|
||||
AuditTargetTypeAvatar AuditTargetType = "avatar" // 头像
|
||||
AuditTargetTypeCover AuditTargetType = "cover" // 背景图
|
||||
AuditTargetTypeBio AuditTargetType = "bio" // 个性签名
|
||||
AuditTargetTypeUserProfile AuditTargetType = "user_profile" // 用户资料审核任务
|
||||
)
|
||||
|
||||
// AuditResult 审核结果
|
||||
type AuditResult string
|
||||
|
||||
const (
|
||||
AuditResultPass AuditResult = "pass" // 通过
|
||||
AuditResultReview AuditResult = "review" // 需人工复审
|
||||
AuditResultBlock AuditResult = "block" // 违规拦截
|
||||
AuditResultUnknown AuditResult = "unknown" // 未知
|
||||
)
|
||||
|
||||
// AuditRiskLevel 风险等级
|
||||
type AuditRiskLevel string
|
||||
|
||||
const (
|
||||
AuditRiskLevelLow AuditRiskLevel = "low" // 低风险
|
||||
AuditRiskLevelMedium AuditRiskLevel = "medium" // 中风险
|
||||
AuditRiskLevelHigh AuditRiskLevel = "high" // 高风险
|
||||
)
|
||||
|
||||
// AuditSource 审核来源
|
||||
type AuditSource string
|
||||
|
||||
const (
|
||||
AuditSourceAuto AuditSource = "auto" // 自动审核
|
||||
AuditSourceManual AuditSource = "manual" // 人工审核
|
||||
AuditSourceCallback AuditSource = "callback" // 回调审核
|
||||
)
|
||||
|
||||
// AuditLog 审核日志实体
|
||||
type AuditLog struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
TargetType AuditTargetType `json:"target_type" gorm:"type:varchar(50);index"`
|
||||
TargetID string `json:"target_id" gorm:"type:varchar(255);index"`
|
||||
Content string `json:"content" gorm:"type:text"` // 待审核内容
|
||||
ContentType string `json:"content_type" gorm:"type:varchar(50)"` // 内容类型: text, image
|
||||
ContentURL string `json:"content_url" gorm:"type:text"` // 图片/文件URL
|
||||
AuditType string `json:"audit_type" gorm:"type:varchar(50)"` // 审核类型: porn, violence, ad, political, fraud, gamble
|
||||
Result AuditResult `json:"result" gorm:"type:varchar(50);index"`
|
||||
RiskLevel AuditRiskLevel `json:"risk_level" gorm:"type:varchar(20)"`
|
||||
Labels string `json:"labels" gorm:"type:text"` // JSON数组,标签列表
|
||||
Suggestion string `json:"suggestion" gorm:"type:varchar(50)"` // pass, review, block
|
||||
Detail string `json:"detail" gorm:"type:text"` // 详细说明
|
||||
ThirdPartyID string `json:"third_party_id" gorm:"type:varchar(255)"` // 第三方审核服务返回的ID
|
||||
Source AuditSource `json:"source" gorm:"type:varchar(20);default:auto"`
|
||||
ReviewerID string `json:"reviewer_id" gorm:"type:varchar(255)"` // 审核人ID(人工审核时使用)
|
||||
ReviewerName string `json:"reviewer_name" gorm:"type:varchar(100)"` // 审核人名称
|
||||
ReviewTime *time.Time `json:"review_time" gorm:"index"` // 审核时间
|
||||
UserID string `json:"user_id" gorm:"type:varchar(255);index"` // 内容发布者ID
|
||||
UserIP string `json:"user_ip" gorm:"type:varchar(45)"` // 用户IP
|
||||
Status string `json:"status" gorm:"type:varchar(20);default:pending"` // pending, completed, failed
|
||||
RejectReason string `json:"reject_reason" gorm:"type:text"` // 拒绝原因(人工审核时使用)
|
||||
ExtraData string `json:"extra_data" gorm:"type:text"` // 额外数据,JSON格式
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (al *AuditLog) BeforeCreate(tx *gorm.DB) error {
|
||||
SetUUIDIfEmpty(&al.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (AuditLog) TableName() string {
|
||||
return "audit_logs"
|
||||
}
|
||||
|
||||
// AuditLogRequest 创建审核日志请求
|
||||
type AuditLogRequest struct {
|
||||
TargetType AuditTargetType `json:"target_type" validate:"required"`
|
||||
TargetID string `json:"target_id" validate:"required"`
|
||||
Content string `json:"content"`
|
||||
ContentType string `json:"content_type"`
|
||||
ContentURL string `json:"content_url"`
|
||||
AuditType string `json:"audit_type"`
|
||||
UserID string `json:"user_id"`
|
||||
UserIP string `json:"user_ip"`
|
||||
}
|
||||
|
||||
// AuditLogListItem 审核日志列表项
|
||||
type AuditLogListItem struct {
|
||||
ID string `json:"id"`
|
||||
TargetType AuditTargetType `json:"target_type"`
|
||||
TargetID string `json:"target_id"`
|
||||
Content string `json:"content"`
|
||||
ContentType string `json:"content_type"`
|
||||
Result AuditResult `json:"result"`
|
||||
RiskLevel AuditRiskLevel `json:"risk_level"`
|
||||
Suggestion string `json:"suggestion"`
|
||||
Source AuditSource `json:"source"`
|
||||
ReviewerID string `json:"reviewer_id"`
|
||||
ReviewTime *time.Time `json:"review_time"`
|
||||
UserID string `json:"user_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func autoMigrate(db *gorm.DB) error {
|
||||
|
||||
// 敏感词和审核
|
||||
&SensitiveWord{},
|
||||
&AuditLog{},
|
||||
// &AuditLog{}, // TODO: define AuditLog model
|
||||
|
||||
// 举报
|
||||
&Report{},
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// BatchDecryptResult 批量解密结果
|
||||
type BatchDecryptResult struct {
|
||||
Index int
|
||||
Plaintext []byte
|
||||
Error error
|
||||
}
|
||||
|
||||
// BatchDecrypt 批量并行解密
|
||||
// workerCount 指定并行工作数,如果 <= 0 则使用 CPU 核数
|
||||
func (e *MessageEncryptor) BatchDecrypt(ciphertexts []string, workerCount int) [][]byte {
|
||||
if e == nil || len(ciphertexts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
results := make([][]byte, len(ciphertexts))
|
||||
|
||||
// 小批量直接串行处理
|
||||
if len(ciphertexts) <= 10 {
|
||||
for i, ct := range ciphertexts {
|
||||
if ct == "" {
|
||||
continue
|
||||
}
|
||||
plaintext, err := e.Decrypt(ct)
|
||||
if err == nil {
|
||||
results[i] = plaintext
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// 并行处理
|
||||
jobs := make(chan int, len(ciphertexts))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 确定工作数
|
||||
if workerCount <= 0 {
|
||||
workerCount = 4 // 默认4个并行
|
||||
}
|
||||
if workerCount > len(ciphertexts) {
|
||||
workerCount = len(ciphertexts)
|
||||
}
|
||||
|
||||
// 启动 worker
|
||||
for w := 0; w < workerCount; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := range jobs {
|
||||
ct := ciphertexts[i]
|
||||
if ct == "" {
|
||||
continue
|
||||
}
|
||||
plaintext, err := e.Decrypt(ct)
|
||||
if err == nil {
|
||||
results[i] = plaintext
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 分发任务
|
||||
for i := range ciphertexts {
|
||||
jobs <- i
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ func (h *Hub) PublishToUserOnline(userID string, eventType string, payload any)
|
||||
}
|
||||
|
||||
// PublishToUserOnlineReliable 可靠地发送事件给在线用户(阻塞发送)
|
||||
// 用于重要的信令消息(如 SDP, ICE candidate),确保消息送达
|
||||
// 用于重要的信令消息(如通话邀请、接听),确保消息送达
|
||||
// 如果用户不在线,返回 false
|
||||
func (h *Hub) PublishToUserOnlineReliable(userID string, eventType string, payload any) bool {
|
||||
h.mu.RLock()
|
||||
|
||||
@@ -39,6 +39,7 @@ type RouterDeps struct {
|
||||
QRCodeHandler *handler.QRCodeHandler
|
||||
MaterialHandler *handler.MaterialHandler
|
||||
CallHandler *handler.CallHandler
|
||||
LiveKitHandler *handler.LiveKitHandler
|
||||
ReportHandler *handler.ReportHandler
|
||||
AdminReportHandler *handler.AdminReportHandler
|
||||
VerificationHandler *handler.VerificationHandler
|
||||
@@ -83,6 +84,7 @@ type Router struct {
|
||||
qrcodeHandler *handler.QRCodeHandler
|
||||
materialHandler *handler.MaterialHandler
|
||||
callHandler *handler.CallHandler
|
||||
liveKitHandler *handler.LiveKitHandler
|
||||
reportHandler *handler.ReportHandler
|
||||
adminReportHandler *handler.AdminReportHandler
|
||||
verificationHandler *handler.VerificationHandler
|
||||
@@ -130,6 +132,7 @@ func New(deps RouterDeps) *Router {
|
||||
qrcodeHandler: deps.QRCodeHandler,
|
||||
materialHandler: deps.MaterialHandler,
|
||||
callHandler: deps.CallHandler,
|
||||
liveKitHandler: deps.LiveKitHandler,
|
||||
reportHandler: deps.ReportHandler,
|
||||
adminReportHandler: deps.AdminReportHandler,
|
||||
verificationHandler: deps.VerificationHandler,
|
||||
@@ -462,10 +465,20 @@ func (r *Router) setupRoutes() {
|
||||
calls.Use(authMiddleware)
|
||||
{
|
||||
calls.GET("/history", r.callHandler.GetCallHistory)
|
||||
calls.GET("/ice-servers", r.callHandler.GetICEServers)
|
||||
}
|
||||
}
|
||||
|
||||
// LiveKit 通话路由
|
||||
if r.liveKitHandler != nil {
|
||||
lk := v1.Group("/calls")
|
||||
lk.Use(authMiddleware)
|
||||
{
|
||||
lk.GET("/token", r.liveKitHandler.GetToken)
|
||||
}
|
||||
// Webhook 不需要认证(LiveKit 服务端调用)
|
||||
v1.POST("/calls/webhook", r.liveKitHandler.HandleWebhook)
|
||||
}
|
||||
|
||||
// 消息操作路由
|
||||
messages := v1.Group("/messages")
|
||||
messages.Use(authMiddleware)
|
||||
|
||||
@@ -59,8 +59,6 @@ type ActiveCall struct {
|
||||
Duration int64 // 通话时长(秒)
|
||||
// 参与者状态
|
||||
Participants map[string]*ActiveParticipant
|
||||
// ICE Servers
|
||||
ICEServers []config.ICEServerConfig
|
||||
}
|
||||
|
||||
// ActiveParticipant 内存中的活跃参与者
|
||||
@@ -77,10 +75,10 @@ type CallService interface {
|
||||
Reject(ctx context.Context, callID, userID string) error
|
||||
Busy(ctx context.Context, callID, userID string) error
|
||||
End(ctx context.Context, callID, userID string, reason string) (*ActiveCall, error)
|
||||
RelaySignal(ctx context.Context, callID, fromUserID string, signalType string, payload json.RawMessage) error
|
||||
SetMuted(ctx context.Context, callID, userID string, muted bool) error
|
||||
Ready(ctx context.Context, callID, userID string) error
|
||||
GetActiveCall(ctx context.Context, callID string) *ActiveCall
|
||||
GetCallHistory(ctx context.Context, userID string, page, pageSize int) ([]model.CallSession, int64, error)
|
||||
GetICEServers() []config.ICEServerConfig
|
||||
StartCleanupTicker()
|
||||
}
|
||||
|
||||
@@ -217,7 +215,6 @@ func (s *callService) redisGetCall(callID string) *ActiveCall {
|
||||
CallType: model.CallType(data.CallType),
|
||||
Status: model.CallStatus(data.Status),
|
||||
MediaType: data.MediaType,
|
||||
ICEServers: s.config.WebRTC.ICEServers,
|
||||
Participants: map[string]*ActiveParticipant{
|
||||
data.CallerID: {UserID: data.CallerID, Status: model.ParticipantStatusJoined},
|
||||
data.CalleeID: {UserID: data.CalleeID, Status: model.ParticipantStatusInvited},
|
||||
@@ -390,7 +387,6 @@ func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversati
|
||||
callerID: {UserID: callerID, Status: model.ParticipantStatusJoined, JoinedAt: &now},
|
||||
calleeID: {UserID: calleeID, Status: model.ParticipantStatusInvited},
|
||||
},
|
||||
ICEServers: s.config.WebRTC.ICEServers,
|
||||
}
|
||||
|
||||
// 存储到内存
|
||||
@@ -411,7 +407,6 @@ func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversati
|
||||
"media_type": mediaType,
|
||||
"created_at": now.UnixMilli(),
|
||||
"lifetime": CallLifetimeMs,
|
||||
"ice_servers": s.config.WebRTC.ICEServers,
|
||||
}
|
||||
|
||||
online := s.hub.PublishToUserOnline(calleeID, "call_incoming", payload)
|
||||
@@ -473,7 +468,6 @@ func (s *callService) Accept(ctx context.Context, callID, userID string) (*Activ
|
||||
s.hub.PublishToUserOnline(call.CallerID, "call_accepted", map[string]any{
|
||||
"call_id": callID,
|
||||
"started_at": now.UnixMilli(),
|
||||
"ice_servers": s.config.WebRTC.ICEServers,
|
||||
})
|
||||
|
||||
// 通知被叫方其他设备
|
||||
@@ -626,35 +620,6 @@ func (s *callService) End(ctx context.Context, callID, userID string, reason str
|
||||
return call, nil
|
||||
}
|
||||
|
||||
func (s *callService) RelaySignal(ctx context.Context, callID, fromUserID string, signalType string, payload json.RawMessage) error {
|
||||
call := s.getActiveCall(callID)
|
||||
if call == nil {
|
||||
return apperrors.ErrCallNotFound
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
if call.Status != model.CallStatusCalling && call.Status != model.CallStatusConnected {
|
||||
s.mu.RUnlock()
|
||||
return apperrors.ErrCallNotActive
|
||||
}
|
||||
if !isParticipant(call, fromUserID) {
|
||||
s.mu.RUnlock()
|
||||
return apperrors.ErrNotCallParticipant
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
for pUserID := range call.Participants {
|
||||
if pUserID != fromUserID {
|
||||
s.hub.PublishToUserOnlineReliable(pUserID, signalType, map[string]any{
|
||||
"call_id": callID,
|
||||
"from_id": fromUserID,
|
||||
"payload": json.RawMessage(payload),
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *callService) SetMuted(ctx context.Context, callID, userID string, muted bool) error {
|
||||
call := s.getActiveCall(callID)
|
||||
if call == nil {
|
||||
@@ -684,13 +649,41 @@ func (s *callService) SetMuted(ctx context.Context, callID, userID string, muted
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveCall 获取活跃通话(公开方法,供 LiveKit handler 调用)
|
||||
func (s *callService) GetActiveCall(ctx context.Context, callID string) *ActiveCall {
|
||||
return s.getActiveCall(callID)
|
||||
}
|
||||
|
||||
// Ready 标记参与者已加入 LiveKit 房间
|
||||
func (s *callService) Ready(ctx context.Context, callID, userID string) error {
|
||||
call := s.getActiveCall(callID)
|
||||
if call == nil {
|
||||
return apperrors.ErrCallNotFound
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
if !isParticipant(call, userID) {
|
||||
s.mu.RUnlock()
|
||||
return apperrors.ErrNotCallParticipant
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
// 通知其他参与者该用户已就绪
|
||||
for pUserID := range call.Participants {
|
||||
if pUserID != userID {
|
||||
s.hub.PublishToUserOnline(pUserID, "call_ready", map[string]any{
|
||||
"call_id": callID,
|
||||
"user_id": userID,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *callService) GetCallHistory(ctx context.Context, userID string, page, pageSize int) ([]model.CallSession, int64, error) {
|
||||
return s.callRepo.GetCallHistory(userID, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *callService) GetICEServers() []config.ICEServerConfig {
|
||||
return s.config.WebRTC.ICEServers
|
||||
}
|
||||
|
||||
// saveCallHistory 保存通话历史到数据库
|
||||
func (s *callService) saveCallHistory(call *ActiveCall, status model.CallStatus, duration int64) {
|
||||
|
||||
191
internal/service/livekit_service.go
Normal file
191
internal/service/livekit_service.go
Normal file
@@ -0,0 +1,191 @@
|
||||
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
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
package wire
|
||||
|
||||
import (
|
||||
"with_you/internal/config"
|
||||
"with_you/internal/handler"
|
||||
"with_you/internal/pkg/ws"
|
||||
"with_you/internal/repository"
|
||||
"with_you/internal/service"
|
||||
|
||||
"github.com/google/wire"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// HandlerSet Handler 层 Provider Set
|
||||
@@ -29,6 +31,7 @@ var HandlerSet = wire.NewSet(
|
||||
handler.NewQRCodeHandler,
|
||||
handler.NewMaterialHandler,
|
||||
handler.NewCallHandler,
|
||||
ProvideLiveKitHandler,
|
||||
handler.NewReportHandler,
|
||||
handler.NewAdminReportHandler,
|
||||
handler.NewVerificationHandler,
|
||||
@@ -106,6 +109,16 @@ func ProvideWSHandler(
|
||||
return handler.NewWSHandler(publisher, chatService, groupService, jwtService, callService, userRepo)
|
||||
}
|
||||
|
||||
// ProvideLiveKitHandler 提供 LiveKit 处理器
|
||||
func ProvideLiveKitHandler(
|
||||
liveKitService service.LiveKitService,
|
||||
callService service.CallService,
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
) *handler.LiveKitHandler {
|
||||
return handler.NewLiveKitHandler(liveKitService, callService, cfg, logger)
|
||||
}
|
||||
|
||||
// ProvideAdminVerificationHandler 提供管理端身份认证处理器
|
||||
func ProvideAdminVerificationHandler(
|
||||
adminVerificationService service.AdminVerificationService,
|
||||
|
||||
@@ -62,6 +62,7 @@ var ServiceSet = wire.NewSet(
|
||||
ProvideHotRankWorker,
|
||||
ProvideMaterialService,
|
||||
ProvideCallService,
|
||||
ProvideLiveKitService,
|
||||
ProvideReportService,
|
||||
ProvideAdminReportService,
|
||||
ProvideVerificationService,
|
||||
@@ -463,6 +464,14 @@ func ProvideCallService(
|
||||
return service.NewCallService(callRepo, publisher, cfg, db, redisClient)
|
||||
}
|
||||
|
||||
// ProvideLiveKitService 提供 LiveKit 服务
|
||||
func ProvideLiveKitService(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
) service.LiveKitService {
|
||||
return service.NewLiveKitService(cfg, logger)
|
||||
}
|
||||
|
||||
// ProvideReportService 提供举报服务
|
||||
func ProvideReportService(
|
||||
reportRepo repository.ReportRepository,
|
||||
|
||||
Reference in New Issue
Block a user