commit 57eba684963fb68a64dd75e3da4789f872a7058c Author: admin Date: Tue Sep 2 11:58:10 2025 +0300 v.0.0.1 Создан сервис клипов diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..f77a70f --- /dev/null +++ b/.drone.yml @@ -0,0 +1,21 @@ +kind: pipeline +name: deploy + +steps: + - name: deploy + image: appleboy/drone-ssh + settings: + host: 89.104.69.222 + username: root + password: + from_secret: ssh_password + script: + - rm -fr /home/tailly_clips + - mkdir /home/tailly_clips + - cd /home/tailly_clips + - git clone https://admin:2bfa8b81e8787c9c0bb89e1a7bbd929b2d63aaf2@git.altomta.ru/admin/tailly_clips . || true + - git pull + - docker stop tailly_clips || true + - docker rm tailly_clips || true + - DOCKER_BUILDKIT=1 docker build -t tailly_clips . + - docker run -d --name tailly_clips --network tailly_net -p 50054:50054 tailly_clips diff --git a/.env b/.env new file mode 100644 index 0000000..7ecb46f --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +DB_HOST=79.174.89.104 +DB_PORT=15452 +DB_USER=tailly_clips +DB_PASSWORD=3Xtk9l0k&DSV789XJ +DB_NAME=tailly_clips + +S3_ENDPOINT=https://s3.regru.cloud +S3_BUCKET=tailly +S3_REGION=ru-central1 +S3_ACCESS_KEY=TJ946G2S1Z5FEI3I7DQQ +S3_SECRET_KEY=C2H2aITHRDpek8H921yhnrINZwDoADsjW3F6HURl + +MODERATION_ADDR=tailly_censor:50051 \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7a7f805 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tailly_clips.iml b/.idea/tailly_clips.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/tailly_clips.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aa5810a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM golang:1.21-alpine AS builder + +# Устанавливаем FFmpeg и зависимости +RUN apk add --no-cache \ + ffmpeg \ + build-base \ + git + +WORKDIR /app + +# Копируем зависимости +COPY go.mod go.sum ./ +RUN go mod download + +# Копируем исходный код +COPY . . + +# Генерируем gRPC код +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 +RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 +RUN protoc --go_out=./gen --go_opt=paths=source_relative \ + --go-grpc_out=./gen --go-grpc_opt=paths=source_relative \ + proto/clip.proto + +# Собираем приложение +RUN go build -o clip-service ./cmd/server + +FROM alpine:3.18 + +# Устанавливаем FFmpeg и зависимости времени выполнения +RUN apk add --no-cache \ + ffmpeg \ + ca-certificates + +WORKDIR /app + +# Копируем бинарник из builder stage +COPY --from=builder /app/clip-service . + +EXPOSE 50054 + +CMD ["./clip-service"] \ No newline at end of file diff --git a/clips_test.go b/clips_test.go new file mode 100644 index 0000000..f7bec47 --- /dev/null +++ b/clips_test.go @@ -0,0 +1,548 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "path/filepath" + "tailly_clips/internal/moderation" + "testing" + "time" + + "tailly_clips/internal/ffmpeg" + "tailly_clips/internal/handler" + "tailly_clips/internal/repository" + "tailly_clips/internal/service" + "tailly_clips/internal/storage" + "tailly_clips/proto" + + "github.com/caarlos0/env/v8" + "github.com/joho/godotenv" + _ "github.com/lib/pq" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type RealTestSuite struct { + grpcClient proto.ClipServiceClient + grpcConn *grpc.ClientConn + grpcServer *grpc.Server + testUserID int32 +} + +type Config struct { + DBHost string `env:"DB_HOST" env-default:"localhost"` + DBPort string `env:"DB_PORT" env-default:"5432"` + DBUser string `env:"DB_USER" env-default:"postgres"` + DBPassword string `env:"DB_PASSWORD" env-default:"postgres"` + DBName string `env:"DB_NAME" env-default:"clip_service"` + + S3Endpoint string `env:"S3_ENDPOINT,required"` + S3Bucket string `env:"S3_BUCKET,required"` + S3Region string `env:"S3_REGION,required"` + S3AccessKey string `env:"S3_ACCESS_KEY,required"` + S3SecretKey string `env:"S3_SECRET_KEY,required"` + + ModerationAddr string `env:"MODERATION_ADDR" env-default:"localhost:50051"` +} + +func loadConfig() (*Config, error) { + // Загружаем .env файл (игнорируем ошибку если файла нет) + _ = godotenv.Load() + + cfg := &Config{} + if err := env.Parse(cfg); err != nil { + return nil, err + } + return cfg, nil +} + +func initDatabase(cfg *Config) (*sql.DB, error) { + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName) + + db, err := sql.Open("postgres", connStr) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + if err = db.Ping(); err != nil { + return nil, fmt.Errorf("failed to ping database: %w", err) + } + + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(25) + db.SetConnMaxLifetime(5 * time.Minute) + + log.Println("Database connected successfully") + return db, nil +} +func setupRealTestSuite(t *testing.T) *RealTestSuite { + // Загружаем конфигурацию из основного .env файла + cfg, err := loadConfig() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + // Инициализация базы данных + db, err := initDatabase(cfg) + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + + // Инициализация S3 storage + s3Storage := storage.NewS3Storage(storage.S3Config{ + Endpoint: cfg.S3Endpoint, + Bucket: cfg.S3Bucket, + Region: cfg.S3Region, + AccessKey: cfg.S3AccessKey, + SecretKey: cfg.S3SecretKey, + }) + + modClient, err := moderation.NewModerationClient(cfg.ModerationAddr) + if err != nil { + log.Fatalf("Failed to create moderation client: %v", err) + } + + // Инициализация сервисов + videoProcessor := ffmpeg.NewVideoProcessor() + clipRepo := repository.NewClipRepository(db) + likeRepo := repository.NewLikeRepository(db) + commentRepo := repository.NewCommentRepository(db) + + clipService := service.NewClipService(clipRepo, s3Storage, videoProcessor, modClient) + likeService := service.NewLikeService(likeRepo, clipRepo) + commentService := service.NewCommentService(commentRepo, clipRepo) + + grpcHandler := handler.NewGRPCHandler(clipService, likeService, commentService) + + // Запускаем gRPC сервер на случайном порту + listener, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("Failed to listen: %v", err) + } + + server := grpc.NewServer( + grpc.MaxRecvMsgSize(50*1024*1024), // 50MB + grpc.MaxSendMsgSize(50*1024*1024), // 50MB + ) + proto.RegisterClipServiceServer(server, grpcHandler) + + go func() { + if err := server.Serve(listener); err != nil { + log.Printf("Server exited with error: %v", err) + } + }() + + // Подключаемся к серверу с увеличенным размером сообщения + conn, err := grpc.Dial(listener.Addr().String(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(50*1024*1024), + grpc.MaxCallSendMsgSize(50*1024*1024), + ), + ) + if err != nil { + t.Fatalf("Failed to dial: %v", err) + } + + client := proto.NewClipServiceClient(conn) + + // Ждем немного для инициализации сервера + time.Sleep(100 * time.Millisecond) + + return &RealTestSuite{ + grpcClient: client, + grpcConn: conn, + grpcServer: server, + testUserID: 9999, // Используем специальный ID для тестов + } +} + +func (s *RealTestSuite) cleanup() { + if s.grpcServer != nil { + s.grpcServer.Stop() + } + if s.grpcConn != nil { + s.grpcConn.Close() + } +} + +func getRealVideoFiles(t *testing.T) []string { + // Укажите путь к директории с реальными видео файлами + videoDir := "./test_videos" // Измените на ваш путь + if _, err := os.Stat(videoDir); os.IsNotExist(err) { + t.Skipf("Video directory %s does not exist, skipping test", videoDir) + return nil + } + + files, err := ioutil.ReadDir(videoDir) + if err != nil { + t.Fatalf("Failed to read video directory: %v", err) + } + + var videoFiles []string + for _, file := range files { + if !file.IsDir() { + ext := filepath.Ext(file.Name()) + if ext == ".mp4" || ext == ".mov" || ext == ".avi" || ext == ".mkv" { + videoFiles = append(videoFiles, filepath.Join(videoDir, file.Name())) + } + } + } + + if len(videoFiles) == 0 { + t.Skip("No video files found in test directory") + } + + // Ограничиваем максимум 10 файлами для теста + if len(videoFiles) > 10 { + videoFiles = videoFiles[:10] + } + + return videoFiles +} + +func TestRealIntegration_UploadMultipleVideos(t *testing.T) { + suite := setupRealTestSuite(t) + defer suite.cleanup() + + ctx := context.Background() + + // Получаем реальные видео файлы + videoFiles := getRealVideoFiles(t) + if videoFiles == nil { + return + } + + t.Logf("Found %d video files for testing", len(videoFiles)) + + // Загружаем каждое видео + for i, videoPath := range videoFiles { + t.Run(fmt.Sprintf("Upload_%s", filepath.Base(videoPath)), func(t *testing.T) { + // Читаем видео файл + videoData, err := ioutil.ReadFile(videoPath) + if err != nil { + t.Fatalf("Failed to read video file %s: %v", videoPath, err) + } + + // Создаем уникальное название для избежания конфликтов + uniqueTitle := fmt.Sprintf("Test Clip %d - %s - %d", + i+1, + filepath.Base(videoPath), + time.Now().Unix()) + + // Создаем запрос + req := &proto.CreateClipRequest{ + UserId: suite.testUserID, + Title: uniqueTitle, + VideoData: videoData, + FileName: filepath.Base(videoPath), + ContentType: "video/mp4", + } + + t.Logf("Uploading %s (%d bytes)", req.FileName, len(videoData)) + + // Вызываем gRPC метод + startTime := time.Now() + resp, err := suite.grpcClient.CreateClip(ctx, req) + uploadTime := time.Since(startTime) + + if err != nil { + t.Fatalf("Failed to create clip: %v", err) + } + + t.Logf("Successfully uploaded in %v: %s (ID: %d)", + uploadTime, resp.Clip.Title, resp.Clip.Id) + + // Проверяем результат + if resp.Clip == nil { + t.Fatal("Expected clip in response") + } + + if resp.Clip.Title != uniqueTitle { + t.Errorf("Expected title %s, got %s", uniqueTitle, resp.Clip.Title) + } + + if resp.Clip.AuthorId != suite.testUserID { + t.Errorf("Expected author ID %d, got %d", suite.testUserID, resp.Clip.AuthorId) + } + + if resp.Clip.Duration <= 0 { + t.Error("Expected positive duration") + } + + if resp.Clip.VideoUrl == "" { + t.Error("Expected video URL") + } + + if resp.Clip.ThumbnailUrl == "" { + t.Error("Expected thumbnail URL") + } + + t.Logf("Video URL: %s", resp.Clip.VideoUrl) + t.Logf("Thumbnail URL: %s", resp.Clip.ThumbnailUrl) + t.Logf("Duration: %d seconds", resp.Clip.Duration) + }) + } +} + +func TestRealIntegration_ClipOperations(t *testing.T) { + suite := setupRealTestSuite(t) + defer suite.cleanup() + + ctx := context.Background() + + // Сначала загружаем тестовое видео + videoFiles := getRealVideoFiles(t) + if videoFiles == nil || len(videoFiles) == 0 { + t.Skip("No video files available for testing") + return + } + + videoData, err := ioutil.ReadFile(videoFiles[0]) + if err != nil { + t.Fatalf("Failed to read test video: %v", err) + } + + // Создаем клип + createResp, err := suite.grpcClient.CreateClip(ctx, &proto.CreateClipRequest{ + UserId: suite.testUserID, + Title: fmt.Sprintf("Integration Test Clip - %d", time.Now().Unix()), + VideoData: videoData, + FileName: filepath.Base(videoFiles[0]), + ContentType: "video/mp4", + }) + if err != nil { + t.Fatalf("Failed to create clip: %v", err) + } + + clipID := createResp.Clip.Id + t.Logf("Created test clip with ID: %d", clipID) + + // Тест 1: Получение клипа + t.Run("Get_clip", func(t *testing.T) { + resp, err := suite.grpcClient.GetClip(ctx, &proto.GetClipRequest{ClipId: clipID}) + if err != nil { + t.Fatalf("Failed to get clip: %v", err) + } + + if resp.Clip.Id != clipID { + t.Errorf("Expected clip ID %d, got %d", clipID, resp.Clip.Id) + } + + t.Logf("Successfully retrieved clip: %s", resp.Clip.Title) + }) + + // Тест 2: Лайк клипа + t.Run("Like_clip", func(t *testing.T) { + _, err := suite.grpcClient.LikeClip(ctx, &proto.LikeClipRequest{ + ClipId: clipID, + UserId: suite.testUserID, + }) + if err != nil { + t.Fatalf("Failed to like clip: %v", err) + } + + // Проверяем что лайк добавился + checkResp, err := suite.grpcClient.CheckIfLiked(ctx, &proto.CheckIfLikedRequest{ + ClipId: clipID, + UserId: suite.testUserID, + }) + if err != nil { + t.Fatalf("Failed to check like: %v", err) + } + + if !checkResp.IsLiked { + t.Error("Expected clip to be liked") + } + + t.Logf("Successfully liked clip %d", clipID) + }) + + // Тест 3: Комментарий + t.Run("Add_comment", func(t *testing.T) { + commentText := fmt.Sprintf("Test comment at %s", time.Now().Format(time.RFC3339)) + commentResp, err := suite.grpcClient.CreateComment(ctx, &proto.CreateCommentRequest{ + ClipId: clipID, + UserId: suite.testUserID, + Content: commentText, + }) + if err != nil { + t.Fatalf("Failed to add comment: %v", err) + } + + if commentResp.Comment.Content != commentText { + t.Errorf("Expected comment '%s', got '%s'", commentText, commentResp.Comment.Content) + } + + t.Logf("Successfully added comment with ID: %d", commentResp.Comment.Id) + }) + + // Тест 4: Получение комментариев + t.Run("Get_comments", func(t *testing.T) { + resp, err := suite.grpcClient.GetClipComments(ctx, &proto.GetClipCommentsRequest{ + ClipId: clipID, + Limit: 10, + Offset: 0, + }) + if err != nil { + t.Fatalf("Failed to get comments: %v", err) + } + + if len(resp.Comments) == 0 { + t.Error("Expected at least one comment") + } else { + t.Logf("Found %d comments for clip %d", len(resp.Comments), clipID) + } + }) + + // Тест 5: Получение списка клипов пользователя + t.Run("Get_user_clips", func(t *testing.T) { + resp, err := suite.grpcClient.GetUserClips(ctx, &proto.GetUserClipsRequest{ + UserId: suite.testUserID, + Limit: 10, + Offset: 0, + }) + if err != nil { + t.Fatalf("Failed to get user clips: %v", err) + } + + t.Logf("User %d has %d clips (total: %d)", + suite.testUserID, len(resp.Clips), resp.TotalCount) + }) + + // Тест 6: Получение всех клипов + t.Run("Get_all_clips", func(t *testing.T) { + resp, err := suite.grpcClient.GetClips(ctx, &proto.GetClipsRequest{ + Limit: 20, + Offset: 0, + }) + if err != nil { + t.Fatalf("Failed to get all clips: %v", err) + } + + t.Logf("Total clips in system: %d", resp.TotalCount) + if len(resp.Clips) > 0 { + t.Logf("Retrieved %d clips", len(resp.Clips)) + } + }) + + // NOTE: Удаление клипа закомментировано, так как данные не должны удаляться + /* + // Тест 7: Удаление клипа + t.Run("Delete_clip", func(t *testing.T) { + _, err := suite.grpcClient.DeleteClip(ctx, &proto.DeleteClipRequest{ + ClipId: clipID, + UserId: suite.testUserID, + }) + if err != nil { + t.Fatalf("Failed to delete clip: %v", err) + } + + t.Logf("Successfully deleted clip %d", clipID) + }) + */ +} + +func TestRealIntegration_PerformanceTest(t *testing.T) { + suite := setupRealTestSuite(t) + defer suite.cleanup() + + ctx := context.Background() + + videoFiles := getRealVideoFiles(t) + if videoFiles == nil || len(videoFiles) < 2 { + t.Skip("Not enough video files for performance test") + return + } + + // Тестируем производительность на первом видео + videoPath := videoFiles[0] + videoData, err := ioutil.ReadFile(videoPath) + if err != nil { + t.Fatalf("Failed to read test video: %v", err) + } + + t.Run("Upload_performance", func(t *testing.T) { + // Замеряем время загрузки + startTime := time.Now() + + _, err := suite.grpcClient.CreateClip(ctx, &proto.CreateClipRequest{ + UserId: suite.testUserID, + Title: fmt.Sprintf("Performance Test - %d", time.Now().UnixNano()), + VideoData: videoData, + FileName: filepath.Base(videoPath), + ContentType: "video/mp4", + }) + + uploadTime := time.Since(startTime) + + if err != nil { + t.Fatalf("Upload failed: %v", err) + } + + t.Logf("Upload performance: %v for %d bytes (%.2f MB/s)", + uploadTime, + len(videoData), + float64(len(videoData))/1024/1024/uploadTime.Seconds()) + }) + + t.Run("Concurrent_uploads", func(t *testing.T) { + // Тестируем конкурентные загрузки (ограниченное количество) + concurrentUploads := 3 + if len(videoFiles) < concurrentUploads { + concurrentUploads = len(videoFiles) + } + + results := make(chan time.Duration, concurrentUploads) + errors := make(chan error, concurrentUploads) + + for i := 0; i < concurrentUploads; i++ { + go func(idx int) { + videoData, err := ioutil.ReadFile(videoFiles[idx]) + if err != nil { + errors <- err + return + } + + startTime := time.Now() + _, err = suite.grpcClient.CreateClip(ctx, &proto.CreateClipRequest{ + UserId: suite.testUserID, + Title: fmt.Sprintf("Concurrent %d - %d", idx, time.Now().UnixNano()), + VideoData: videoData, + FileName: filepath.Base(videoFiles[idx]), + ContentType: "video/mp4", + }) + uploadTime := time.Since(startTime) + + if err != nil { + errors <- err + return + } + + results <- uploadTime + }(i) + } + + // Собираем результаты + var totalTime time.Duration + for i := 0; i < concurrentUploads; i++ { + select { + case duration := <-results: + totalTime += duration + t.Logf("Concurrent upload %d: %v", i+1, duration) + case err := <-errors: + t.Errorf("Concurrent upload failed: %v", err) + case <-time.After(2 * time.Minute): + t.Error("Concurrent upload timeout") + } + } + + avgTime := totalTime / time.Duration(concurrentUploads) + t.Logf("Average concurrent upload time: %v", avgTime) + }) +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..231a824 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net" + "tailly_clips/internal/ffmpeg" + "tailly_clips/internal/handler" + "tailly_clips/internal/moderation" + "tailly_clips/internal/repository" + "tailly_clips/internal/service" + "tailly_clips/internal/storage" + "tailly_clips/proto" + "time" + + "github.com/caarlos0/env/v8" + "github.com/joho/godotenv" + _ "github.com/lib/pq" + "google.golang.org/grpc" +) + +// Config структура конфигурации +type Config struct { + DBHost string `env:"DB_HOST" env-default:"localhost"` + DBPort string `env:"DB_PORT" env-default:"5432"` + DBUser string `env:"DB_USER" env-default:"postgres"` + DBPassword string `env:"DB_PASSWORD" env-default:"postgres"` + DBName string `env:"DB_NAME" env-default:"clip_service"` + + S3Endpoint string `env:"S3_ENDPOINT,required"` + S3Bucket string `env:"S3_BUCKET,required"` + S3Region string `env:"S3_REGION,required"` + S3AccessKey string `env:"S3_ACCESS_KEY,required"` + S3SecretKey string `env:"S3_SECRET_KEY,required"` + + ModerationAddr string `env:"MODERATION_ADDR" env-default:"localhost:50051"` +} + +func main() { + // Загрузка конфигурации + cfg, err := loadConfig() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + // Инициализация базы данных + db, err := initDatabase(cfg) + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + defer db.Close() + + // Инициализация S3 storage + s3Storage := storage.NewS3Storage(storage.S3Config{ + Endpoint: cfg.S3Endpoint, + Bucket: cfg.S3Bucket, + Region: cfg.S3Region, + AccessKey: cfg.S3AccessKey, + SecretKey: cfg.S3SecretKey, + }) + + modClient, err := moderation.NewModerationClient(cfg.ModerationAddr) + if err != nil { + log.Fatalf("Failed to create moderation client: %v", err) + } + defer modClient.Close() + + videoProcessor := ffmpeg.NewVideoProcessor() + clipRepo := repository.NewClipRepository(db) + likeRepo := repository.NewLikeRepository(db) + commentRepo := repository.NewCommentRepository(db) + + clipService := service.NewClipService(clipRepo, s3Storage, videoProcessor, modClient) + likeService := service.NewLikeService(likeRepo, clipRepo) + commentService := service.NewCommentService(commentRepo, clipRepo) + + grpcHandler := handler.NewGRPCHandler(clipService, likeService, commentService) + + server := grpc.NewServer() + proto.RegisterClipServiceServer(server, grpcHandler) + + lis, err := net.Listen("tcp", ":50054") + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + + log.Println("Clip service started on :50054") + if err := server.Serve(lis); err != nil { + log.Fatalf("Failed to serve: %v", err) + } +} + +func loadConfig() (*Config, error) { + // Загружаем .env файл (игнорируем ошибку если файла нет) + _ = godotenv.Load() + + cfg := &Config{} + if err := env.Parse(cfg); err != nil { + return nil, err + } + return cfg, nil +} + +func initDatabase(cfg *Config) (*sql.DB, error) { + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName) + + db, err := sql.Open("postgres", connStr) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + if err = db.Ping(); err != nil { + return nil, fmt.Errorf("failed to ping database: %w", err) + } + + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(25) + db.SetConnMaxLifetime(5 * time.Minute) + + log.Println("Database connected successfully") + return db, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fa2208e --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module tailly_clips + +go 1.25 + +require ( + github.com/aws/aws-sdk-go v1.49.6 + github.com/caarlos0/env/v8 v8.0.0 + github.com/joho/godotenv v1.5.1 + google.golang.org/grpc v1.67.0 + google.golang.org/protobuf v1.36.8 +) + +require github.com/lib/pq v1.10.9 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0fa2d6d --- /dev/null +++ b/go.sum @@ -0,0 +1,36 @@ +github.com/aws/aws-sdk-go v1.49.6 h1:yNldzF5kzLBRvKlKz1S0bkvc2+04R1kt13KfBWQBfFA= +github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0= +github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo= +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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= diff --git a/internal/domain/models.go b/internal/domain/models.go new file mode 100644 index 0000000..a3f7dc4 --- /dev/null +++ b/internal/domain/models.go @@ -0,0 +1,40 @@ +package domain + +import "time" + +type Clip struct { + ID int `json:"id"` + Title string `json:"title"` + VideoURL string `json:"video_url"` + ThumbnailURL string `json:"thumbnail_url"` + Duration int `json:"duration"` // seconds + AuthorID int `json:"author_id"` + LikesCount int `json:"likes_count"` + CommentsCount int `json:"comments_count"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ClipLike struct { + ID int `json:"id"` + ClipID int `json:"clip_id"` + UserID int `json:"user_id"` + CreatedAt time.Time `json:"created_at"` +} + +type ClipComment struct { + ID int `json:"id"` + ClipID int `json:"clip_id"` + AuthorID int `json:"author_id"` + Content string `json:"content"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateClipRequest struct { + UserID int + Title string + VideoData []byte + FileName string + ContentType string +} diff --git a/internal/ffmpeg/processor.go b/internal/ffmpeg/processor.go new file mode 100644 index 0000000..712b1c2 --- /dev/null +++ b/internal/ffmpeg/processor.go @@ -0,0 +1,114 @@ +package ffmpeg + +import ( + "bytes" + "fmt" + "os/exec" + "strconv" + "strings" +) + +type VideoProcessor interface { + GenerateThumbnail(videoData []byte) ([]byte, error) + GetDuration(videoData []byte) (int, error) + TrimVideo(videoData []byte, maxDuration int) ([]byte, error) +} + +type videoProcessor struct { +} + +func NewVideoProcessor() VideoProcessor { + return &videoProcessor{} +} + +func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) { + cmd := exec.Command("ffmpeg", + "-i", "pipe:0", // читаем из stdin + "-ss", "00:00:01", + "-vframes", "1", + "-q:v", "2", + "-f", "image2pipe", // вывод в pipe + "-c:v", "mjpeg", + "pipe:1", // пишем в stdout + ) + + cmd.Stdin = bytes.NewReader(videoData) + var output bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &output // capture stderr for error messages + + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("ffmpeg failed: %s, error: %w", output.String(), err) + } + + if output.Len() == 0 { + return nil, fmt.Errorf("thumbnail generation produced empty output") + } + + return output.Bytes(), nil +} + +func (vp *videoProcessor) GetDuration(videoData []byte) (int, error) { + cmd := exec.Command("ffprobe", + "-i", "pipe:0", + "-show_entries", "format=duration", + "-v", "quiet", + "-of", "csv=p=0", + ) + + cmd.Stdin = bytes.NewReader(videoData) + output, err := cmd.Output() + if err != nil { + debugCmd := exec.Command("ffprobe", "-i", "pipe:0") + debugCmd.Stdin = bytes.NewReader(videoData) + debugOutput, _ := debugCmd.CombinedOutput() + return 0, fmt.Errorf("ffprobe failed: %w, debug output: %s", err, string(debugOutput)) + } + + durationStr := strings.TrimSpace(string(output)) + duration, err := strconv.ParseFloat(durationStr, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse duration '%s': %w", durationStr, err) + } + + return int(duration), nil +} + +func (vp *videoProcessor) TrimVideo(videoData []byte, maxDuration int) ([]byte, error) { + // Сначала получаем длительность + duration, err := vp.GetDuration(videoData) + if err != nil { + return nil, fmt.Errorf("failed to get video duration: %w", err) + } + + // Если видео короче или равно лимиту, возвращаем оригинал + if duration <= maxDuration { + return videoData, nil + } + + // Обрезаем видео + cmd := exec.Command("ffmpeg", + "-i", "pipe:0", + "-t", strconv.Itoa(maxDuration), + "-c", "copy", + "-f", "mp4", + "pipe:1", + ) + + cmd.Stdin = bytes.NewReader(videoData) + var output bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &output + + err = cmd.Run() + if err != nil { + return nil, fmt.Errorf("ffmpeg trim failed: %s, error: %w", output.String(), err) + } + + if output.Len() == 0 { + return nil, fmt.Errorf("trimmed video is empty") + } + + return output.Bytes(), nil +} diff --git a/internal/handler/grpc_handler.go b/internal/handler/grpc_handler.go new file mode 100644 index 0000000..9a1cecc --- /dev/null +++ b/internal/handler/grpc_handler.go @@ -0,0 +1,243 @@ +package handler + +import ( + "context" + "tailly_clips/internal/domain" + "tailly_clips/internal/service" + "tailly_clips/proto" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type GRPCHandler struct { + proto.UnimplementedClipServiceServer + clipService service.ClipService + likeService service.LikeService + commentService service.CommentService +} + +func NewGRPCHandler( + clipService service.ClipService, + likeService service.LikeService, + commentService service.CommentService, +) *GRPCHandler { + return &GRPCHandler{ + clipService: clipService, + likeService: likeService, + commentService: commentService, + } +} + +func (h *GRPCHandler) CreateClip(ctx context.Context, req *proto.CreateClipRequest) (*proto.CreateClipResponse, error) { + createReq := domain.CreateClipRequest{ + UserID: int(req.UserId), + Title: req.Title, + VideoData: req.VideoData, + FileName: req.FileName, + ContentType: req.ContentType, + } + + clip, err := h.clipService.CreateClip(ctx, createReq) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create clip: %v", err) + } + + return &proto.CreateClipResponse{ + Clip: h.clipToProto(clip), + }, nil +} + +func (h *GRPCHandler) GetClip(ctx context.Context, req *proto.GetClipRequest) (*proto.GetClipResponse, error) { + clip, err := h.clipService.GetClip(ctx, int(req.ClipId)) + if err != nil { + return nil, status.Errorf(codes.NotFound, "clip not found: %v", err) + } + + return &proto.GetClipResponse{ + Clip: h.clipToProto(clip), + }, nil +} + +func (h *GRPCHandler) GetUserClips(ctx context.Context, req *proto.GetUserClipsRequest) (*proto.GetUserClipsResponse, error) { + clips, totalCount, err := h.clipService.GetUserClips(ctx, int(req.UserId), int(req.Limit), int(req.Offset)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user clips: %v", err) + } + + return &proto.GetUserClipsResponse{ + Clips: h.clipsToProto(clips), + TotalCount: int32(totalCount), + }, nil +} + +func (h *GRPCHandler) GetClips(ctx context.Context, req *proto.GetClipsRequest) (*proto.GetClipsResponse, error) { + clips, totalCount, err := h.clipService.GetClips(ctx, int(req.Limit), int(req.Offset)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get clips: %v", err) + } + + return &proto.GetClipsResponse{ + Clips: h.clipsToProto(clips), + TotalCount: int32(totalCount), + }, nil +} + +func (h *GRPCHandler) DeleteClip(ctx context.Context, req *proto.DeleteClipRequest) (*emptypb.Empty, error) { + err := h.clipService.DeleteClip(ctx, int(req.ClipId), int(req.UserId)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete clip: %v", err) + } + + return &emptypb.Empty{}, nil +} + +func (h *GRPCHandler) LikeClip(ctx context.Context, req *proto.LikeClipRequest) (*proto.LikeClipResponse, error) { + like, err := h.likeService.LikeClip(ctx, int(req.ClipId), int(req.UserId)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to like clip: %v", err) + } + + return &proto.LikeClipResponse{ + Like: h.likeToProto(like), + }, nil +} + +func (h *GRPCHandler) UnlikeClip(ctx context.Context, req *proto.UnlikeClipRequest) (*emptypb.Empty, error) { + err := h.likeService.UnlikeClip(ctx, int(req.ClipId), int(req.UserId)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to unlike clip: %v", err) + } + + return &emptypb.Empty{}, nil +} + +func (h *GRPCHandler) GetClipLikes(ctx context.Context, req *proto.GetClipLikesRequest) (*proto.GetClipLikesResponse, error) { + likes, totalCount, err := h.likeService.GetClipLikes(ctx, int(req.ClipId), int(req.Limit), int(req.Offset)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get clip likes: %v", err) + } + + return &proto.GetClipLikesResponse{ + Likes: h.likesToProto(likes), + TotalCount: int32(totalCount), + }, nil +} + +func (h *GRPCHandler) CheckIfLiked(ctx context.Context, req *proto.CheckIfLikedRequest) (*proto.CheckIfLikedResponse, error) { + isLiked, err := h.likeService.CheckIfLiked(ctx, int(req.ClipId), int(req.UserId)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to check like status: %v", err) + } + + return &proto.CheckIfLikedResponse{ + IsLiked: isLiked, + }, nil +} + +func (h *GRPCHandler) CreateComment(ctx context.Context, req *proto.CreateCommentRequest) (*proto.CreateCommentResponse, error) { + comment, err := h.commentService.CreateComment(ctx, int(req.ClipId), int(req.UserId), req.Content) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create comment: %v", err) + } + + return &proto.CreateCommentResponse{ + Comment: h.commentToProto(comment), + }, nil +} + +func (h *GRPCHandler) GetClipComments(ctx context.Context, req *proto.GetClipCommentsRequest) (*proto.GetClipCommentsResponse, error) { + comments, totalCount, err := h.commentService.GetClipComments(ctx, int(req.ClipId), int(req.Limit), int(req.Offset)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get clip comments: %v", err) + } + + return &proto.GetClipCommentsResponse{ + Comments: h.commentsToProto(comments), + TotalCount: int32(totalCount), + }, nil +} + +func (h *GRPCHandler) DeleteComment(ctx context.Context, req *proto.DeleteCommentRequest) (*emptypb.Empty, error) { + err := h.commentService.DeleteComment(ctx, int(req.CommentId), int(req.UserId)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete comment: %v", err) + } + + return &emptypb.Empty{}, nil +} + +// Вспомогательные методы для конвертации + +func (h *GRPCHandler) clipToProto(clip *domain.Clip) *proto.Clip { + if clip == nil { + return nil + } + + return &proto.Clip{ + Id: int32(clip.ID), + Title: clip.Title, + VideoUrl: clip.VideoURL, + ThumbnailUrl: clip.ThumbnailURL, + Duration: int32(clip.Duration), + AuthorId: int32(clip.AuthorID), + LikesCount: int32(clip.LikesCount), + CommentsCount: int32(clip.CommentsCount), + CreatedAt: timestamppb.New(clip.CreatedAt), + UpdatedAt: timestamppb.New(clip.UpdatedAt), + } +} + +func (h *GRPCHandler) clipsToProto(clips []*domain.Clip) []*proto.Clip { + var protoClips []*proto.Clip + for _, clip := range clips { + protoClips = append(protoClips, h.clipToProto(clip)) + } + return protoClips +} + +func (h *GRPCHandler) likeToProto(like *domain.ClipLike) *proto.ClipLike { + if like == nil { + return nil + } + + return &proto.ClipLike{ + Id: int32(like.ID), + ClipId: int32(like.ClipID), + UserId: int32(like.UserID), + CreatedAt: timestamppb.New(like.CreatedAt), + } +} + +func (h *GRPCHandler) likesToProto(likes []*domain.ClipLike) []*proto.ClipLike { + var protoLikes []*proto.ClipLike + for _, like := range likes { + protoLikes = append(protoLikes, h.likeToProto(like)) + } + return protoLikes +} + +func (h *GRPCHandler) commentToProto(comment *domain.ClipComment) *proto.ClipComment { + if comment == nil { + return nil + } + + return &proto.ClipComment{ + Id: int32(comment.ID), + ClipId: int32(comment.ClipID), + AuthorId: int32(comment.AuthorID), + Content: comment.Content, + CreatedAt: timestamppb.New(comment.CreatedAt), + UpdatedAt: timestamppb.New(comment.UpdatedAt), + } +} + +func (h *GRPCHandler) commentsToProto(comments []*domain.ClipComment) []*proto.ClipComment { + var protoComments []*proto.ClipComment + for _, comment := range comments { + protoComments = append(protoComments, h.commentToProto(comment)) + } + return protoComments +} diff --git a/internal/moderation/moderation.go b/internal/moderation/moderation.go new file mode 100644 index 0000000..3a91136 --- /dev/null +++ b/internal/moderation/moderation.go @@ -0,0 +1,39 @@ +package moderation + +import ( + "context" + + pb "tailly_clips/internal/moderation/proto" + + "google.golang.org/grpc" +) + +type ModerationClient struct { + conn *grpc.ClientConn + client pb.ModerationServiceClient +} + +func NewModerationClient(addr string) (*ModerationClient, error) { + conn, err := grpc.Dial(addr, grpc.WithInsecure()) + if err != nil { + return nil, err + } + + return &ModerationClient{ + conn: conn, + client: pb.NewModerationServiceClient(conn), + }, nil +} + +func (c *ModerationClient) Close() { + c.conn.Close() +} +func (c *ModerationClient) CheckVideoUrl(ctx context.Context, videoUrl string) (bool, error) { + resp, err := c.client.CheckVideo(ctx, &pb.VideoRequest{ + VideoUrl: videoUrl, + }) + if err != nil { + return false, err + } + return resp.OverallDecision == "allowed", nil +} diff --git a/internal/moderation/proto/moderation.pb.go b/internal/moderation/proto/moderation.pb.go new file mode 100644 index 0000000..f91adb6 --- /dev/null +++ b/internal/moderation/proto/moderation.pb.go @@ -0,0 +1,400 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc v3.21.12 +// source: moderation.proto + +package __ + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ImageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ImageData []byte `protobuf:"bytes,1,opt,name=image_data,json=imageData,proto3" json:"image_data,omitempty"` + ImageUrl string `protobuf:"bytes,2,opt,name=image_url,json=imageUrl,proto3" json:"image_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImageRequest) Reset() { + *x = ImageRequest{} + mi := &file_moderation_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImageRequest) ProtoMessage() {} + +func (x *ImageRequest) ProtoReflect() protoreflect.Message { + mi := &file_moderation_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImageRequest.ProtoReflect.Descriptor instead. +func (*ImageRequest) Descriptor() ([]byte, []int) { + return file_moderation_proto_rawDescGZIP(), []int{0} +} + +func (x *ImageRequest) GetImageData() []byte { + if x != nil { + return x.ImageData + } + return nil +} + +func (x *ImageRequest) GetImageUrl() string { + if x != nil { + return x.ImageUrl + } + return "" +} + +type VideoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + VideoUrl string `protobuf:"bytes,1,opt,name=video_url,json=videoUrl,proto3" json:"video_url,omitempty"` + FrameSampleRate int32 `protobuf:"varint,2,opt,name=frame_sample_rate,json=frameSampleRate,proto3" json:"frame_sample_rate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VideoRequest) Reset() { + *x = VideoRequest{} + mi := &file_moderation_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VideoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoRequest) ProtoMessage() {} + +func (x *VideoRequest) ProtoReflect() protoreflect.Message { + mi := &file_moderation_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VideoRequest.ProtoReflect.Descriptor instead. +func (*VideoRequest) Descriptor() ([]byte, []int) { + return file_moderation_proto_rawDescGZIP(), []int{1} +} + +func (x *VideoRequest) GetVideoUrl() string { + if x != nil { + return x.VideoUrl + } + return "" +} + +func (x *VideoRequest) GetFrameSampleRate() int32 { + if x != nil { + return x.FrameSampleRate + } + return 0 +} + +type ModerationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + OverallDecision string `protobuf:"bytes,1,opt,name=overall_decision,json=overallDecision,proto3" json:"overall_decision,omitempty"` + Predictions []*Prediction `protobuf:"bytes,2,rep,name=predictions,proto3" json:"predictions,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModerationResponse) Reset() { + *x = ModerationResponse{} + mi := &file_moderation_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModerationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModerationResponse) ProtoMessage() {} + +func (x *ModerationResponse) ProtoReflect() protoreflect.Message { + mi := &file_moderation_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModerationResponse.ProtoReflect.Descriptor instead. +func (*ModerationResponse) Descriptor() ([]byte, []int) { + return file_moderation_proto_rawDescGZIP(), []int{2} +} + +func (x *ModerationResponse) GetOverallDecision() string { + if x != nil { + return x.OverallDecision + } + return "" +} + +func (x *ModerationResponse) GetPredictions() []*Prediction { + if x != nil { + return x.Predictions + } + return nil +} + +func (x *ModerationResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type Prediction struct { + state protoimpl.MessageState `protogen:"open.v1"` + Category string `protobuf:"bytes,1,opt,name=category,proto3" json:"category,omitempty"` + Score float32 `protobuf:"fixed32,2,opt,name=score,proto3" json:"score,omitempty"` + Decision string `protobuf:"bytes,3,opt,name=decision,proto3" json:"decision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Prediction) Reset() { + *x = Prediction{} + mi := &file_moderation_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Prediction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Prediction) ProtoMessage() {} + +func (x *Prediction) ProtoReflect() protoreflect.Message { + mi := &file_moderation_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Prediction.ProtoReflect.Descriptor instead. +func (*Prediction) Descriptor() ([]byte, []int) { + return file_moderation_proto_rawDescGZIP(), []int{3} +} + +func (x *Prediction) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *Prediction) GetScore() float32 { + if x != nil { + return x.Score + } + return 0 +} + +func (x *Prediction) GetDecision() string { + if x != nil { + return x.Decision + } + return "" +} + +type ModerationSettings struct { + state protoimpl.MessageState `protogen:"open.v1"` + CategoryThresholds map[string]float32 `protobuf:"bytes,1,rep,name=category_thresholds,json=categoryThresholds,proto3" json:"category_thresholds,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"fixed32,2,opt,name=value"` + AlwaysBlockCategories []string `protobuf:"bytes,2,rep,name=always_block_categories,json=alwaysBlockCategories,proto3" json:"always_block_categories,omitempty"` + AlwaysAllowCategories []string `protobuf:"bytes,3,rep,name=always_allow_categories,json=alwaysAllowCategories,proto3" json:"always_allow_categories,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModerationSettings) Reset() { + *x = ModerationSettings{} + mi := &file_moderation_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModerationSettings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModerationSettings) ProtoMessage() {} + +func (x *ModerationSettings) ProtoReflect() protoreflect.Message { + mi := &file_moderation_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModerationSettings.ProtoReflect.Descriptor instead. +func (*ModerationSettings) Descriptor() ([]byte, []int) { + return file_moderation_proto_rawDescGZIP(), []int{4} +} + +func (x *ModerationSettings) GetCategoryThresholds() map[string]float32 { + if x != nil { + return x.CategoryThresholds + } + return nil +} + +func (x *ModerationSettings) GetAlwaysBlockCategories() []string { + if x != nil { + return x.AlwaysBlockCategories + } + return nil +} + +func (x *ModerationSettings) GetAlwaysAllowCategories() []string { + if x != nil { + return x.AlwaysAllowCategories + } + return nil +} + +var File_moderation_proto protoreflect.FileDescriptor + +const file_moderation_proto_rawDesc = "" + + "\n" + + "\x10moderation.proto\x12\n" + + "moderation\"J\n" + + "\fImageRequest\x12\x1d\n" + + "\n" + + "image_data\x18\x01 \x01(\fR\timageData\x12\x1b\n" + + "\timage_url\x18\x02 \x01(\tR\bimageUrl\"W\n" + + "\fVideoRequest\x12\x1b\n" + + "\tvideo_url\x18\x01 \x01(\tR\bvideoUrl\x12*\n" + + "\x11frame_sample_rate\x18\x02 \x01(\x05R\x0fframeSampleRate\"\x93\x01\n" + + "\x12ModerationResponse\x12)\n" + + "\x10overall_decision\x18\x01 \x01(\tR\x0foverallDecision\x128\n" + + "\vpredictions\x18\x02 \x03(\v2\x16.moderation.PredictionR\vpredictions\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\"Z\n" + + "\n" + + "Prediction\x12\x1a\n" + + "\bcategory\x18\x01 \x01(\tR\bcategory\x12\x14\n" + + "\x05score\x18\x02 \x01(\x02R\x05score\x12\x1a\n" + + "\bdecision\x18\x03 \x01(\tR\bdecision\"\xb4\x02\n" + + "\x12ModerationSettings\x12g\n" + + "\x13category_thresholds\x18\x01 \x03(\v26.moderation.ModerationSettings.CategoryThresholdsEntryR\x12categoryThresholds\x126\n" + + "\x17always_block_categories\x18\x02 \x03(\tR\x15alwaysBlockCategories\x126\n" + + "\x17always_allow_categories\x18\x03 \x03(\tR\x15alwaysAllowCategories\x1aE\n" + + "\x17CategoryThresholdsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\x02R\x05value:\x028\x012\xa7\x01\n" + + "\x11ModerationService\x12H\n" + + "\n" + + "CheckImage\x12\x18.moderation.ImageRequest\x1a\x1e.moderation.ModerationResponse\"\x00\x12H\n" + + "\n" + + "CheckVideo\x12\x18.moderation.VideoRequest\x1a\x1e.moderation.ModerationResponse\"\x00B\x03Z\x01.b\x06proto3" + +var ( + file_moderation_proto_rawDescOnce sync.Once + file_moderation_proto_rawDescData []byte +) + +func file_moderation_proto_rawDescGZIP() []byte { + file_moderation_proto_rawDescOnce.Do(func() { + file_moderation_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_moderation_proto_rawDesc), len(file_moderation_proto_rawDesc))) + }) + return file_moderation_proto_rawDescData +} + +var file_moderation_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_moderation_proto_goTypes = []any{ + (*ImageRequest)(nil), // 0: moderation.ImageRequest + (*VideoRequest)(nil), // 1: moderation.VideoRequest + (*ModerationResponse)(nil), // 2: moderation.ModerationResponse + (*Prediction)(nil), // 3: moderation.Prediction + (*ModerationSettings)(nil), // 4: moderation.ModerationSettings + nil, // 5: moderation.ModerationSettings.CategoryThresholdsEntry +} +var file_moderation_proto_depIdxs = []int32{ + 3, // 0: moderation.ModerationResponse.predictions:type_name -> moderation.Prediction + 5, // 1: moderation.ModerationSettings.category_thresholds:type_name -> moderation.ModerationSettings.CategoryThresholdsEntry + 0, // 2: moderation.ModerationService.CheckImage:input_type -> moderation.ImageRequest + 1, // 3: moderation.ModerationService.CheckVideo:input_type -> moderation.VideoRequest + 2, // 4: moderation.ModerationService.CheckImage:output_type -> moderation.ModerationResponse + 2, // 5: moderation.ModerationService.CheckVideo:output_type -> moderation.ModerationResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_moderation_proto_init() } +func file_moderation_proto_init() { + if File_moderation_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_moderation_proto_rawDesc), len(file_moderation_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_moderation_proto_goTypes, + DependencyIndexes: file_moderation_proto_depIdxs, + MessageInfos: file_moderation_proto_msgTypes, + }.Build() + File_moderation_proto = out.File + file_moderation_proto_goTypes = nil + file_moderation_proto_depIdxs = nil +} diff --git a/internal/moderation/proto/moderation.proto b/internal/moderation/proto/moderation.proto new file mode 100644 index 0000000..0d31e75 --- /dev/null +++ b/internal/moderation/proto/moderation.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package moderation; +option go_package = "."; + +service ModerationService { + rpc CheckImage (ImageRequest) returns (ModerationResponse) {} + rpc CheckVideo (VideoRequest) returns (ModerationResponse) {} +} + +message ImageRequest { + bytes image_data = 1; + string image_url = 2; +} + +message VideoRequest { + string video_url = 1; + int32 frame_sample_rate = 2; +} + +message ModerationResponse { + string overall_decision = 1; + repeated Prediction predictions = 2; + string message = 3; +} + +message Prediction { + string category = 1; + float score = 2; + string decision = 3; +} + +message ModerationSettings { + map category_thresholds = 1; + repeated string always_block_categories = 2; + repeated string always_allow_categories = 3; +} \ No newline at end of file diff --git a/internal/moderation/proto/moderation_grpc.pb.go b/internal/moderation/proto/moderation_grpc.pb.go new file mode 100644 index 0000000..484086e --- /dev/null +++ b/internal/moderation/proto/moderation_grpc.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v3.21.12 +// source: moderation.proto + +package __ + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ModerationService_CheckImage_FullMethodName = "/moderation.ModerationService/CheckImage" + ModerationService_CheckVideo_FullMethodName = "/moderation.ModerationService/CheckVideo" +) + +// ModerationServiceClient is the client API for ModerationService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ModerationServiceClient interface { + CheckImage(ctx context.Context, in *ImageRequest, opts ...grpc.CallOption) (*ModerationResponse, error) + CheckVideo(ctx context.Context, in *VideoRequest, opts ...grpc.CallOption) (*ModerationResponse, error) +} + +type moderationServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewModerationServiceClient(cc grpc.ClientConnInterface) ModerationServiceClient { + return &moderationServiceClient{cc} +} + +func (c *moderationServiceClient) CheckImage(ctx context.Context, in *ImageRequest, opts ...grpc.CallOption) (*ModerationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ModerationResponse) + err := c.cc.Invoke(ctx, ModerationService_CheckImage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *moderationServiceClient) CheckVideo(ctx context.Context, in *VideoRequest, opts ...grpc.CallOption) (*ModerationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ModerationResponse) + err := c.cc.Invoke(ctx, ModerationService_CheckVideo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ModerationServiceServer is the server API for ModerationService service. +// All implementations must embed UnimplementedModerationServiceServer +// for forward compatibility. +type ModerationServiceServer interface { + CheckImage(context.Context, *ImageRequest) (*ModerationResponse, error) + CheckVideo(context.Context, *VideoRequest) (*ModerationResponse, error) + mustEmbedUnimplementedModerationServiceServer() +} + +// UnimplementedModerationServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedModerationServiceServer struct{} + +func (UnimplementedModerationServiceServer) CheckImage(context.Context, *ImageRequest) (*ModerationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckImage not implemented") +} +func (UnimplementedModerationServiceServer) CheckVideo(context.Context, *VideoRequest) (*ModerationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckVideo not implemented") +} +func (UnimplementedModerationServiceServer) mustEmbedUnimplementedModerationServiceServer() {} +func (UnimplementedModerationServiceServer) testEmbeddedByValue() {} + +// UnsafeModerationServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ModerationServiceServer will +// result in compilation errors. +type UnsafeModerationServiceServer interface { + mustEmbedUnimplementedModerationServiceServer() +} + +func RegisterModerationServiceServer(s grpc.ServiceRegistrar, srv ModerationServiceServer) { + // If the following call pancis, it indicates UnimplementedModerationServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ModerationService_ServiceDesc, srv) +} + +func _ModerationService_CheckImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ModerationServiceServer).CheckImage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ModerationService_CheckImage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ModerationServiceServer).CheckImage(ctx, req.(*ImageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ModerationService_CheckVideo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VideoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ModerationServiceServer).CheckVideo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ModerationService_CheckVideo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ModerationServiceServer).CheckVideo(ctx, req.(*VideoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ModerationService_ServiceDesc is the grpc.ServiceDesc for ModerationService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ModerationService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "moderation.ModerationService", + HandlerType: (*ModerationServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CheckImage", + Handler: _ModerationService_CheckImage_Handler, + }, + { + MethodName: "CheckVideo", + Handler: _ModerationService_CheckVideo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "moderation.proto", +} diff --git a/internal/repository/clip_repository.go b/internal/repository/clip_repository.go new file mode 100644 index 0000000..ceb534d --- /dev/null +++ b/internal/repository/clip_repository.go @@ -0,0 +1,335 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "fmt" + "tailly_clips/internal/domain" + "time" +) + +var ( + ErrClipNotFound = errors.New("clip not found") +) + +type ClipRepository interface { + Create(ctx context.Context, clip *domain.Clip) error + GetByID(ctx context.Context, id int) (*domain.Clip, error) + GetByAuthorID(ctx context.Context, authorID, limit, offset int) ([]*domain.Clip, int, error) + GetAll(ctx context.Context, limit, offset int) ([]*domain.Clip, int, error) + Delete(ctx context.Context, id int) error + IncrementLikesCount(ctx context.Context, clipID int) error + DecrementLikesCount(ctx context.Context, clipID int) error + IncrementCommentsCount(ctx context.Context, clipID int) error + DecrementCommentsCount(ctx context.Context, clipID int) error + GetClipURLs(ctx context.Context, clipID int) (string, string, error) + GetClipWithURLs(ctx context.Context, clipID int) (*domain.Clip, error) +} + +type clipRepository struct { + db *sql.DB +} + +func NewClipRepository(db *sql.DB) ClipRepository { + return &clipRepository{db: db} +} + +func (r *clipRepository) Create(ctx context.Context, clip *domain.Clip) error { + query := ` + INSERT INTO clips (title, video_url, thumbnail_url, duration, author_id, likes_count, comments_count, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id + ` + + err := r.db.QueryRowContext(ctx, query, + clip.Title, + clip.VideoURL, + clip.ThumbnailURL, + clip.Duration, + clip.AuthorID, + clip.LikesCount, + clip.CommentsCount, + clip.CreatedAt, + clip.UpdatedAt, + ).Scan(&clip.ID) + + if err != nil { + return fmt.Errorf("failed to create clip: %w", err) + } + + return nil +} + +func (r *clipRepository) GetByID(ctx context.Context, id int) (*domain.Clip, error) { + query := ` + SELECT id, title, video_url, thumbnail_url, duration, author_id, + likes_count, comments_count, created_at, updated_at + FROM clips + WHERE id = $1 AND deleted_at IS NULL + ` + + clip := &domain.Clip{} + err := r.db.QueryRowContext(ctx, query, id).Scan( + &clip.ID, + &clip.Title, + &clip.VideoURL, + &clip.ThumbnailURL, + &clip.Duration, + &clip.AuthorID, + &clip.LikesCount, + &clip.CommentsCount, + &clip.CreatedAt, + &clip.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, ErrClipNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get clip: %w", err) + } + + return clip, nil +} + +func (r *clipRepository) GetByAuthorID(ctx context.Context, authorID, limit, offset int) ([]*domain.Clip, int, error) { + // Получаем общее количество + countQuery := `SELECT COUNT(*) FROM clips WHERE author_id = $1 AND deleted_at IS NULL` + var totalCount int + err := r.db.QueryRowContext(ctx, countQuery, authorID).Scan(&totalCount) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Получаем клипы + query := ` + SELECT id, title, video_url, thumbnail_url, duration, author_id, + likes_count, comments_count, created_at, updated_at + FROM clips + WHERE author_id = $1 AND deleted_at IS NULL + ORDER BY created_at DESC + LIMIT $2 OFFSET $3 + ` + + rows, err := r.db.QueryContext(ctx, query, authorID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get clips: %w", err) + } + defer rows.Close() + + var clips []*domain.Clip + for rows.Next() { + clip := &domain.Clip{} + err := rows.Scan( + &clip.ID, + &clip.Title, + &clip.VideoURL, + &clip.ThumbnailURL, + &clip.Duration, + &clip.AuthorID, + &clip.LikesCount, + &clip.CommentsCount, + &clip.CreatedAt, + &clip.UpdatedAt, + ) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan clip: %w", err) + } + clips = append(clips, clip) + } + + if err = rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows error: %w", err) + } + + return clips, totalCount, nil +} + +func (r *clipRepository) GetAll(ctx context.Context, limit, offset int) ([]*domain.Clip, int, error) { + // Получаем общее количество + countQuery := `SELECT COUNT(*) FROM clips WHERE deleted_at IS NULL` + var totalCount int + err := r.db.QueryRowContext(ctx, countQuery).Scan(&totalCount) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Получаем клипы + query := ` + SELECT id, title, video_url, thumbnail_url, duration, author_id, + likes_count, comments_count, created_at, updated_at + FROM clips + WHERE deleted_at IS NULL + ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + ` + + rows, err := r.db.QueryContext(ctx, query, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get clips: %w", err) + } + defer rows.Close() + + var clips []*domain.Clip + for rows.Next() { + clip := &domain.Clip{} + err := rows.Scan( + &clip.ID, + &clip.Title, + &clip.VideoURL, + &clip.ThumbnailURL, + &clip.Duration, + &clip.AuthorID, + &clip.LikesCount, + &clip.CommentsCount, + &clip.CreatedAt, + &clip.UpdatedAt, + ) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan clip: %w", err) + } + clips = append(clips, clip) + } + + if err = rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows error: %w", err) + } + + return clips, totalCount, nil +} + +func (r *clipRepository) Delete(ctx context.Context, id int) error { + query := ` + UPDATE clips + SET deleted_at = $1 + WHERE id = $2 AND deleted_at IS NULL + ` + + result, err := r.db.ExecContext(ctx, query, time.Now(), id) + if err != nil { + return fmt.Errorf("failed to delete clip: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return ErrClipNotFound + } + + return nil +} + +func (r *clipRepository) IncrementLikesCount(ctx context.Context, clipID int) error { + query := ` + UPDATE clips + SET likes_count = likes_count + 1, updated_at = $1 + WHERE id = $2 AND deleted_at IS NULL + ` + + _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) + if err != nil { + return fmt.Errorf("failed to increment likes count: %w", err) + } + + return nil +} + +func (r *clipRepository) DecrementLikesCount(ctx context.Context, clipID int) error { + query := ` + UPDATE clips + SET likes_count = GREATEST(likes_count - 1, 0), updated_at = $1 + WHERE id = $2 AND deleted_at IS NULL + ` + + _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) + if err != nil { + return fmt.Errorf("failed to decrement likes count: %w", err) + } + + return nil +} + +func (r *clipRepository) IncrementCommentsCount(ctx context.Context, clipID int) error { + query := ` + UPDATE clips + SET comments_count = comments_count + 1, updated_at = $1 + WHERE id = $2 AND deleted_at IS NULL + ` + + _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) + if err != nil { + return fmt.Errorf("failed to increment comments count: %w", err) + } + + return nil +} + +func (r *clipRepository) DecrementCommentsCount(ctx context.Context, clipID int) error { + query := ` + UPDATE clips + SET comments_count = GREATEST(comments_count - 1, 0), updated_at = $1 + WHERE id = $2 AND deleted_at IS NULL + ` + + _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) + if err != nil { + return fmt.Errorf("failed to decrement comments count: %w", err) + } + + return nil +} +func (r *clipRepository) GetClipURLs(ctx context.Context, clipID int) (string, string, error) { + query := ` + SELECT video_url, thumbnail_url + FROM clips + WHERE id = $1 AND deleted_at IS NULL + ` + + var videoURL, thumbnailURL string + err := r.db.QueryRowContext(ctx, query, clipID).Scan(&videoURL, &thumbnailURL) + if err == sql.ErrNoRows { + return "", "", ErrClipNotFound + } + if err != nil { + return "", "", fmt.Errorf("failed to get clip URLs: %w", err) + } + + return videoURL, thumbnailURL, nil +} + +// GetClipWithURLs возвращает клип с URL +func (r *clipRepository) GetClipWithURLs(ctx context.Context, clipID int) (*domain.Clip, error) { + query := ` + SELECT id, title, video_url, thumbnail_url, duration, author_id, + likes_count, comments_count, created_at, updated_at + FROM clips + WHERE id = $1 AND deleted_at IS NULL + ` + + clip := &domain.Clip{} + err := r.db.QueryRowContext(ctx, query, clipID).Scan( + &clip.ID, + &clip.Title, + &clip.VideoURL, + &clip.ThumbnailURL, + &clip.Duration, + &clip.AuthorID, + &clip.LikesCount, + &clip.CommentsCount, + &clip.CreatedAt, + &clip.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, ErrClipNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get clip: %w", err) + } + + return clip, nil +} diff --git a/internal/repository/comment_repository.go b/internal/repository/comment_repository.go new file mode 100644 index 0000000..8d4fdd0 --- /dev/null +++ b/internal/repository/comment_repository.go @@ -0,0 +1,262 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "fmt" + "tailly_clips/internal/domain" + "time" +) + +var ( + ErrCommentNotFound = errors.New("comment not found") +) + +type CommentRepository interface { + Create(ctx context.Context, comment *domain.ClipComment) error + GetByID(ctx context.Context, id int) (*domain.ClipComment, error) + GetByClipID(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipComment, int, error) + GetByAuthorID(ctx context.Context, authorID, limit, offset int) ([]*domain.ClipComment, int, error) + Update(ctx context.Context, comment *domain.ClipComment) error + Delete(ctx context.Context, id int) error + DeleteByClipID(ctx context.Context, clipID int) error + DeleteByAuthorID(ctx context.Context, authorID int) error +} + +type commentRepository struct { + db *sql.DB +} + +func NewCommentRepository(db *sql.DB) CommentRepository { + return &commentRepository{db: db} +} + +func (r *commentRepository) Create(ctx context.Context, comment *domain.ClipComment) error { + query := ` + INSERT INTO clip_comments (clip_id, author_id, content, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5) + RETURNING id + ` + + err := r.db.QueryRowContext(ctx, query, + comment.ClipID, + comment.AuthorID, + comment.Content, + comment.CreatedAt, + comment.UpdatedAt, + ).Scan(&comment.ID) + + if err != nil { + return fmt.Errorf("failed to create comment: %w", err) + } + + return nil +} + +func (r *commentRepository) GetByID(ctx context.Context, id int) (*domain.ClipComment, error) { + query := ` + SELECT id, clip_id, author_id, content, created_at, updated_at + FROM clip_comments + WHERE id = $1 AND deleted_at IS NULL + ` + + comment := &domain.ClipComment{} + err := r.db.QueryRowContext(ctx, query, id).Scan( + &comment.ID, + &comment.ClipID, + &comment.AuthorID, + &comment.Content, + &comment.CreatedAt, + &comment.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, ErrCommentNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get comment: %w", err) + } + + return comment, nil +} + +func (r *commentRepository) GetByClipID(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipComment, int, error) { + // Получаем общее количество + countQuery := `SELECT COUNT(*) FROM clip_comments WHERE clip_id = $1 AND deleted_at IS NULL` + var totalCount int + err := r.db.QueryRowContext(ctx, countQuery, clipID).Scan(&totalCount) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Получаем комментарии + query := ` + SELECT id, clip_id, author_id, content, created_at, updated_at + FROM clip_comments + WHERE clip_id = $1 AND deleted_at IS NULL + ORDER BY created_at ASC + LIMIT $2 OFFSET $3 + ` + + rows, err := r.db.QueryContext(ctx, query, clipID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get comments: %w", err) + } + defer rows.Close() + + var comments []*domain.ClipComment + for rows.Next() { + comment := &domain.ClipComment{} + err := rows.Scan( + &comment.ID, + &comment.ClipID, + &comment.AuthorID, + &comment.Content, + &comment.CreatedAt, + &comment.UpdatedAt, + ) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan comment: %w", err) + } + comments = append(comments, comment) + } + + if err = rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows error: %w", err) + } + + return comments, totalCount, nil +} + +func (r *commentRepository) GetByAuthorID(ctx context.Context, authorID, limit, offset int) ([]*domain.ClipComment, int, error) { + // Получаем общее количество + countQuery := `SELECT COUNT(*) FROM clip_comments WHERE author_id = $1 AND deleted_at IS NULL` + var totalCount int + err := r.db.QueryRowContext(ctx, countQuery, authorID).Scan(&totalCount) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Получаем комментарии + query := ` + SELECT id, clip_id, author_id, content, created_at, updated_at + FROM clip_comments + WHERE author_id = $1 AND deleted_at IS NULL + ORDER BY created_at DESC + LIMIT $2 OFFSET $3 + ` + + rows, err := r.db.QueryContext(ctx, query, authorID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get comments: %w", err) + } + defer rows.Close() + + var comments []*domain.ClipComment + for rows.Next() { + comment := &domain.ClipComment{} + err := rows.Scan( + &comment.ID, + &comment.ClipID, + &comment.AuthorID, + &comment.Content, + &comment.CreatedAt, + &comment.UpdatedAt, + ) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan comment: %w", err) + } + comments = append(comments, comment) + } + + if err = rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows error: %w", err) + } + + return comments, totalCount, nil +} + +func (r *commentRepository) Update(ctx context.Context, comment *domain.ClipComment) error { + query := ` + UPDATE clip_comments + SET content = $1, updated_at = $2 + WHERE id = $3 AND deleted_at IS NULL + ` + + result, err := r.db.ExecContext(ctx, query, + comment.Content, + time.Now(), + comment.ID, + ) + + if err != nil { + return fmt.Errorf("failed to update comment: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return ErrCommentNotFound + } + + comment.UpdatedAt = time.Now() + return nil +} + +func (r *commentRepository) Delete(ctx context.Context, id int) error { + query := ` + UPDATE clip_comments + SET deleted_at = $1 + WHERE id = $2 AND deleted_at IS NULL + ` + + result, err := r.db.ExecContext(ctx, query, time.Now(), id) + if err != nil { + return fmt.Errorf("failed to delete comment: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return ErrCommentNotFound + } + + return nil +} + +func (r *commentRepository) DeleteByClipID(ctx context.Context, clipID int) error { + query := ` + UPDATE clip_comments + SET deleted_at = $1 + WHERE clip_id = $2 AND deleted_at IS NULL + ` + + _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) + if err != nil { + return fmt.Errorf("failed to delete comments by clip ID: %w", err) + } + + return nil +} + +func (r *commentRepository) DeleteByAuthorID(ctx context.Context, authorID int) error { + query := ` + UPDATE clip_comments + SET deleted_at = $1 + WHERE author_id = $2 AND deleted_at IS NULL + ` + + _, err := r.db.ExecContext(ctx, query, time.Now(), authorID) + if err != nil { + return fmt.Errorf("failed to delete comments by author ID: %w", err) + } + + return nil +} diff --git a/internal/repository/like_repository.go b/internal/repository/like_repository.go new file mode 100644 index 0000000..f2eeebf --- /dev/null +++ b/internal/repository/like_repository.go @@ -0,0 +1,242 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "fmt" + "tailly_clips/internal/domain" +) + +var ( + ErrLikeNotFound = errors.New("like not found") + ErrLikeAlreadyExists = errors.New("like already exists") +) + +type LikeRepository interface { + Create(ctx context.Context, like *domain.ClipLike) error + GetByID(ctx context.Context, id int) (*domain.ClipLike, error) + GetByClipID(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipLike, int, error) + GetByUserID(ctx context.Context, userID, limit, offset int) ([]*domain.ClipLike, int, error) + CheckExists(ctx context.Context, clipID, userID int) (bool, error) + Delete(ctx context.Context, clipID, userID int) error + DeleteByClipID(ctx context.Context, clipID int) error + DeleteByUserID(ctx context.Context, userID int) error +} + +type likeRepository struct { + db *sql.DB +} + +func NewLikeRepository(db *sql.DB) LikeRepository { + return &likeRepository{db: db} +} + +func (r *likeRepository) Create(ctx context.Context, like *domain.ClipLike) error { + query := ` + INSERT INTO clip_likes (clip_id, user_id, created_at) + VALUES ($1, $2, $3) + RETURNING id + ` + + err := r.db.QueryRowContext(ctx, query, + like.ClipID, + like.UserID, + like.CreatedAt, + ).Scan(&like.ID) + + if err != nil { + if isDuplicateKeyError(err) { + return ErrLikeAlreadyExists + } + return fmt.Errorf("failed to create like: %w", err) + } + + return nil +} + +func (r *likeRepository) GetByID(ctx context.Context, id int) (*domain.ClipLike, error) { + query := ` + SELECT id, clip_id, user_id, created_at + FROM clip_likes + WHERE id = $1 + ` + + like := &domain.ClipLike{} + err := r.db.QueryRowContext(ctx, query, id).Scan( + &like.ID, + &like.ClipID, + &like.UserID, + &like.CreatedAt, + ) + + if err == sql.ErrNoRows { + return nil, ErrLikeNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get like: %w", err) + } + + return like, nil +} + +func (r *likeRepository) GetByClipID(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipLike, int, error) { + // Получаем общее количество + countQuery := `SELECT COUNT(*) FROM clip_likes WHERE clip_id = $1` + var totalCount int + err := r.db.QueryRowContext(ctx, countQuery, clipID).Scan(&totalCount) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Получаем лайки + query := ` + SELECT id, clip_id, user_id, created_at + FROM clip_likes + WHERE clip_id = $1 + ORDER BY created_at DESC + LIMIT $2 OFFSET $3 + ` + + rows, err := r.db.QueryContext(ctx, query, clipID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get likes: %w", err) + } + defer rows.Close() + + var likes []*domain.ClipLike + for rows.Next() { + like := &domain.ClipLike{} + err := rows.Scan( + &like.ID, + &like.ClipID, + &like.UserID, + &like.CreatedAt, + ) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan like: %w", err) + } + likes = append(likes, like) + } + + if err = rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows error: %w", err) + } + + return likes, totalCount, nil +} + +func (r *likeRepository) GetByUserID(ctx context.Context, userID, limit, offset int) ([]*domain.ClipLike, int, error) { + // Получаем общее количество + countQuery := `SELECT COUNT(*) FROM clip_likes WHERE user_id = $1` + var totalCount int + err := r.db.QueryRowContext(ctx, countQuery, userID).Scan(&totalCount) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Получаем лайки + query := ` + SELECT id, clip_id, user_id, created_at + FROM clip_likes + WHERE user_id = $1 + ORDER BY created_at DESC + LIMIT $2 OFFSET $3 + ` + + rows, err := r.db.QueryContext(ctx, query, userID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get likes: %w", err) + } + defer rows.Close() + + var likes []*domain.ClipLike + for rows.Next() { + like := &domain.ClipLike{} + err := rows.Scan( + &like.ID, + &like.ClipID, + &like.UserID, + &like.CreatedAt, + ) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan like: %w", err) + } + likes = append(likes, like) + } + + if err = rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows error: %w", err) + } + + return likes, totalCount, nil +} + +func (r *likeRepository) CheckExists(ctx context.Context, clipID, userID int) (bool, error) { + query := ` + SELECT EXISTS( + SELECT 1 FROM clip_likes + WHERE clip_id = $1 AND user_id = $2 + ) + ` + + var exists bool + err := r.db.QueryRowContext(ctx, query, clipID, userID).Scan(&exists) + if err != nil { + return false, fmt.Errorf("failed to check like existence: %w", err) + } + + return exists, nil +} + +func (r *likeRepository) Delete(ctx context.Context, clipID, userID int) error { + query := ` + DELETE FROM clip_likes + WHERE clip_id = $1 AND user_id = $2 + ` + + result, err := r.db.ExecContext(ctx, query, clipID, userID) + if err != nil { + return fmt.Errorf("failed to delete like: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return ErrLikeNotFound + } + + return nil +} + +func (r *likeRepository) DeleteByClipID(ctx context.Context, clipID int) error { + query := `DELETE FROM clip_likes WHERE clip_id = $1` + + _, err := r.db.ExecContext(ctx, query, clipID) + if err != nil { + return fmt.Errorf("failed to delete likes by clip ID: %w", err) + } + + return nil +} + +func (r *likeRepository) DeleteByUserID(ctx context.Context, userID int) error { + query := `DELETE FROM clip_likes WHERE user_id = $1` + + _, err := r.db.ExecContext(ctx, query, userID) + if err != nil { + return fmt.Errorf("failed to delete likes by user ID: %w", err) + } + + return nil +} + +func isDuplicateKeyError(err error) bool { + // Проверка на дубликат ключа (зависит от драйвера БД) + // Для PostgreSQL обычно содержит "duplicate key" + return err != nil && (err.Error() == "pq: duplicate key value violates unique constraint" || + err.Error() == "ERROR: duplicate key value violates unique constraint") +} diff --git a/internal/service/clip_service.go b/internal/service/clip_service.go new file mode 100644 index 0000000..fc8b2ad --- /dev/null +++ b/internal/service/clip_service.go @@ -0,0 +1,217 @@ +package service + +import ( + "context" + "fmt" + "log" + "tailly_clips/internal/domain" + "tailly_clips/internal/ffmpeg" + "tailly_clips/internal/moderation" + "tailly_clips/internal/repository" + "tailly_clips/internal/storage" + "time" +) + +type ModerationClient interface { + CheckVideoUrl(ctx context.Context, videoUrl string) (bool, error) + Close() +} +type ClipService interface { + CreateClip(ctx context.Context, req domain.CreateClipRequest) (*domain.Clip, error) + GetClip(ctx context.Context, clipID int) (*domain.Clip, error) + GetUserClips(ctx context.Context, userID, limit, offset int) ([]*domain.Clip, int, error) + GetClips(ctx context.Context, limit, offset int) ([]*domain.Clip, int, error) + DeleteClip(ctx context.Context, clipID, userID int) error +} + +type clipService struct { + repo repository.ClipRepository + storage storage.Storage + videoProcessor ffmpeg.VideoProcessor + modClient *moderation.ModerationClient +} + +func NewClipService(repo repository.ClipRepository, storage storage.Storage, processor ffmpeg.VideoProcessor, modClient *moderation.ModerationClient) ClipService { + return &clipService{ + repo: repo, + storage: storage, + videoProcessor: processor, + modClient: modClient, + } +} + +func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipRequest) (*domain.Clip, error) { + const op = "service/clipService.CreateClip" + + // 1. Обрезаем видео до 30 секунд в памяти + trimmedVideoData, err := s.videoProcessor.TrimVideo(req.VideoData, 30) + if err != nil { + return nil, fmt.Errorf("%s: failed to trim video: %w", op, err) + } + + // 2. Параллельно выполняем операции + videoURLChan := make(chan string, 1) + videoErrChan := make(chan error, 1) + thumbnailChan := make(chan string, 1) + thumbnailErrChan := make(chan error, 1) + durationChan := make(chan int, 1) + durationErrChan := make(chan error, 1) + + // Горутина для загрузки видео в S3 + go func() { + videoURL, err := s.storage.UploadVideo(ctx, trimmedVideoData, req.FileName, req.UserID) + if err != nil { + videoErrChan <- fmt.Errorf("%s: failed to upload video: %w", op, err) + return + } + videoURLChan <- videoURL + }() + + // Горутина для генерации превью + go func() { + thumbnailData, err := s.videoProcessor.GenerateThumbnail(trimmedVideoData) + if err != nil { + thumbnailErrChan <- fmt.Errorf("%s: failed to generate thumbnail: %w", op, err) + return + } + + thumbnailURL, err := s.storage.UploadThumbnail(ctx, thumbnailData, req.UserID) + if err != nil { + thumbnailErrChan <- fmt.Errorf("%s: failed to upload thumbnail: %w", op, err) + return + } + thumbnailChan <- thumbnailURL + }() + + // Горутина для получения длительности + go func() { + duration, err := s.videoProcessor.GetDuration(trimmedVideoData) + if err != nil { + durationErrChan <- fmt.Errorf("%s: failed to get duration: %w", op, err) + return + } + durationChan <- duration + }() + + // 3. Ждем результаты всех операций + var videoURL, thumbnailURL string + var duration int + + // Ждем загрузки видео + select { + case videoURL = <-videoURLChan: + case err := <-videoErrChan: + return nil, err + case <-ctx.Done(): + return nil, fmt.Errorf("%s: operation cancelled", op) + } + + // Ждем генерации превью + select { + case thumbnailURL = <-thumbnailChan: + case err := <-thumbnailErrChan: + s.storage.DeleteVideo(ctx, videoURL) + return nil, err + case <-ctx.Done(): + s.storage.DeleteVideo(ctx, videoURL) + return nil, fmt.Errorf("%s: operation cancelled", op) + } + + // Ждем получения длительности + select { + case duration = <-durationChan: + case err := <-durationErrChan: + s.storage.DeleteVideo(ctx, videoURL) + s.storage.DeleteThumbnail(ctx, thumbnailURL) + return nil, err + case <-ctx.Done(): + s.storage.DeleteVideo(ctx, videoURL) + s.storage.DeleteThumbnail(ctx, thumbnailURL) + return nil, fmt.Errorf("%s: operation cancelled", op) + } + + // 4. Модерируем обрезанное видео по URL + videoAllowed, err := s.modClient.CheckVideoUrl(ctx, videoURL) + if err != nil { + s.storage.DeleteVideo(ctx, videoURL) + s.storage.DeleteThumbnail(ctx, thumbnailURL) + return nil, fmt.Errorf("%s: video moderation failed: %w", op, err) + } + if !videoAllowed { + s.storage.DeleteVideo(ctx, videoURL) + s.storage.DeleteThumbnail(ctx, thumbnailURL) + return nil, fmt.Errorf("%s: video rejected by moderation service", op) + } + + // 5. Создаем клип в БД + clip := &domain.Clip{ + Title: req.Title, + VideoURL: videoURL, + ThumbnailURL: thumbnailURL, + Duration: duration, + AuthorID: req.UserID, + LikesCount: 0, + CommentsCount: 0, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := s.repo.Create(ctx, clip); err != nil { + s.storage.DeleteVideo(ctx, videoURL) + s.storage.DeleteThumbnail(ctx, thumbnailURL) + return nil, fmt.Errorf("failed to create clip: %w", err) + } + + return clip, nil +} + +func (s *clipService) GetClip(ctx context.Context, clipID int) (*domain.Clip, error) { + clip, err := s.repo.GetByID(ctx, clipID) + if err != nil { + return nil, fmt.Errorf("failed to get clip: %w", err) + } + return clip, nil +} + +func (s *clipService) GetUserClips(ctx context.Context, userID, limit, offset int) ([]*domain.Clip, int, error) { + clips, totalCount, err := s.repo.GetByAuthorID(ctx, userID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get user clips: %w", err) + } + return clips, totalCount, nil +} + +func (s *clipService) GetClips(ctx context.Context, limit, offset int) ([]*domain.Clip, int, error) { + clips, totalCount, err := s.repo.GetAll(ctx, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get clips: %w", err) + } + return clips, totalCount, nil +} + +func (s *clipService) DeleteClip(ctx context.Context, clipID, userID int) error { + // Получаем клип для проверки прав и получения URL + clip, err := s.repo.GetByID(ctx, clipID) + if err != nil { + return fmt.Errorf("failed to get clip: %w", err) + } + + // Проверяем права доступа + if clip.AuthorID != userID { + return fmt.Errorf("user %d is not the author of clip %d", userID, clipID) + } + + // Удаляем файлы из S3 + fileURLs := []string{clip.VideoURL, clip.ThumbnailURL} + if err := s.storage.BulkDelete(ctx, fileURLs); err != nil { + return fmt.Errorf("failed to delete files from S3: %w", err) + } + + // Удаляем клип из БД + if err := s.repo.Delete(ctx, clipID); err != nil { + // Логируем ошибку но не возвращаем её, так как файлы уже удалены + log.Printf("WARNING: Failed to delete clip %d from database after S3 deletion: %v", clipID, err) + } + + return nil +} diff --git a/internal/service/comment_service.go b/internal/service/comment_service.go new file mode 100644 index 0000000..bfd178f --- /dev/null +++ b/internal/service/comment_service.go @@ -0,0 +1,103 @@ +package service + +import ( + "context" + "fmt" + "tailly_clips/internal/domain" + "tailly_clips/internal/repository" + "time" +) + +type CommentService interface { + CreateComment(ctx context.Context, clipID, userID int, content string) (*domain.ClipComment, error) + GetClipComments(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipComment, int, error) + DeleteComment(ctx context.Context, commentID, userID int) error +} + +type commentService struct { + commentRepo repository.CommentRepository + clipRepo repository.ClipRepository +} + +func NewCommentService(commentRepo repository.CommentRepository, clipRepo repository.ClipRepository) CommentService { + return &commentService{ + commentRepo: commentRepo, + clipRepo: clipRepo, + } +} + +func (s *commentService) CreateComment(ctx context.Context, clipID, userID int, content string) (*domain.ClipComment, error) { + // Проверяем существование клипа + if _, err := s.clipRepo.GetByID(ctx, clipID); err != nil { + return nil, fmt.Errorf("clip not found: %w", err) + } + + // Валидация контента + if content == "" { + return nil, fmt.Errorf("comment content cannot be empty") + } + if len(content) > 1000 { + return nil, fmt.Errorf("comment content too long") + } + + // Создаем комментарий + comment := &domain.ClipComment{ + ClipID: clipID, + AuthorID: userID, + Content: content, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := s.commentRepo.Create(ctx, comment); err != nil { + return nil, fmt.Errorf("failed to create comment: %w", err) + } + + // Увеличиваем счетчик комментариев + if err := s.clipRepo.IncrementCommentsCount(ctx, clipID); err != nil { + // Откатываем создание комментария при ошибке + s.commentRepo.Delete(ctx, comment.ID) + return nil, fmt.Errorf("failed to increment comments count: %w", err) + } + + return comment, nil +} + +func (s *commentService) GetClipComments(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipComment, int, error) { + // Проверяем существование клипа + if _, err := s.clipRepo.GetByID(ctx, clipID); err != nil { + return nil, 0, fmt.Errorf("clip not found: %w", err) + } + + comments, totalCount, err := s.commentRepo.GetByClipID(ctx, clipID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get clip comments: %w", err) + } + + return comments, totalCount, nil +} + +func (s *commentService) DeleteComment(ctx context.Context, commentID, userID int) error { + // Получаем комментарий для проверки прав + comment, err := s.commentRepo.GetByID(ctx, commentID) + if err != nil { + return fmt.Errorf("failed to get comment: %w", err) + } + + // Проверяем права доступа + if comment.AuthorID != userID { + return fmt.Errorf("user %d is not the author of comment %d", userID, commentID) + } + + // Удаляем комментарий + if err := s.commentRepo.Delete(ctx, commentID); err != nil { + return fmt.Errorf("failed to delete comment: %w", err) + } + + // Уменьшаем счетчик комментариев + if err := s.clipRepo.DecrementCommentsCount(ctx, comment.ClipID); err != nil { + return fmt.Errorf("failed to decrement comments count: %w", err) + } + + return nil +} diff --git a/internal/service/like_service.go b/internal/service/like_service.go new file mode 100644 index 0000000..6af4fb6 --- /dev/null +++ b/internal/service/like_service.go @@ -0,0 +1,111 @@ +package service + +import ( + "context" + "fmt" + "tailly_clips/internal/domain" + "tailly_clips/internal/repository" + "time" +) + +type LikeService interface { + LikeClip(ctx context.Context, clipID, userID int) (*domain.ClipLike, error) + UnlikeClip(ctx context.Context, clipID, userID int) error + GetClipLikes(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipLike, int, error) + CheckIfLiked(ctx context.Context, clipID, userID int) (bool, error) +} + +type likeService struct { + likeRepo repository.LikeRepository + clipRepo repository.ClipRepository +} + +func NewLikeService(likeRepo repository.LikeRepository, clipRepo repository.ClipRepository) LikeService { + return &likeService{ + likeRepo: likeRepo, + clipRepo: clipRepo, + } +} + +func (s *likeService) LikeClip(ctx context.Context, clipID, userID int) (*domain.ClipLike, error) { + // Проверяем существование клипа + if _, err := s.clipRepo.GetByID(ctx, clipID); err != nil { + return nil, fmt.Errorf("clip not found: %w", err) + } + + // Проверяем не лайкнул ли уже пользователь + alreadyLiked, err := s.likeRepo.CheckExists(ctx, clipID, userID) + if err != nil { + return nil, fmt.Errorf("failed to check like existence: %w", err) + } + if alreadyLiked { + return nil, fmt.Errorf("user already liked this clip") + } + + // Создаем лайк + like := &domain.ClipLike{ + ClipID: clipID, + UserID: userID, + CreatedAt: time.Now(), + } + + if err := s.likeRepo.Create(ctx, like); err != nil { + return nil, fmt.Errorf("failed to create like: %w", err) + } + + // Увеличиваем счетчик лайков + if err := s.clipRepo.IncrementLikesCount(ctx, clipID); err != nil { + // Откатываем создание лайка при ошибке + s.likeRepo.Delete(ctx, clipID, userID) + return nil, fmt.Errorf("failed to increment likes count: %w", err) + } + + return like, nil +} + +func (s *likeService) UnlikeClip(ctx context.Context, clipID, userID int) error { + // Проверяем существование клипа + if _, err := s.clipRepo.GetByID(ctx, clipID); err != nil { + return fmt.Errorf("clip not found: %w", err) + } + + // Удаляем лайк + if err := s.likeRepo.Delete(ctx, clipID, userID); err != nil { + return fmt.Errorf("failed to delete like: %w", err) + } + + // Уменьшаем счетчик лайков + if err := s.clipRepo.DecrementLikesCount(ctx, clipID); err != nil { + return fmt.Errorf("failed to decrement likes count: %w", err) + } + + return nil +} + +func (s *likeService) GetClipLikes(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipLike, int, error) { + // Проверяем существование клипа + if _, err := s.clipRepo.GetByID(ctx, clipID); err != nil { + return nil, 0, fmt.Errorf("clip not found: %w", err) + } + + likes, totalCount, err := s.likeRepo.GetByClipID(ctx, clipID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get clip likes: %w", err) + } + + return likes, totalCount, nil +} + +func (s *likeService) CheckIfLiked(ctx context.Context, clipID, userID int) (bool, error) { + // Проверяем существование клипа + if _, err := s.clipRepo.GetByID(ctx, clipID); err != nil { + return false, fmt.Errorf("clip not found: %w", err) + } + + isLiked, err := s.likeRepo.CheckExists(ctx, clipID, userID) + if err != nil { + return false, fmt.Errorf("failed to check like status: %w", err) + } + + return isLiked, nil +} diff --git a/internal/storage/s3_storage.go b/internal/storage/s3_storage.go new file mode 100644 index 0000000..a76d24d --- /dev/null +++ b/internal/storage/s3_storage.go @@ -0,0 +1,240 @@ +package storage + +import ( + "bytes" + "context" + "fmt" + "path/filepath" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" +) + +type Storage interface { + UploadVideo(ctx context.Context, videoData []byte, fileName string, userID int) (string, error) + UploadThumbnail(ctx context.Context, thumbnailData []byte, userID int) (string, error) + Delete(ctx context.Context, fileURL string) error + DeleteVideo(ctx context.Context, videoURL string) error + DeleteThumbnail(ctx context.Context, thumbnailURL string) error + BulkDelete(ctx context.Context, fileURLs []string) error + CheckObjectExists(ctx context.Context, fileURL string) (bool, error) +} + +type S3Config struct { + Endpoint string + Bucket string + Region string + AccessKey string + SecretKey string +} + +type s3Storage struct { + uploader *s3manager.Uploader + s3Client *s3.S3 + bucket string + region string + endpoint string +} + +// NewS3Storage создает S3 storage - теперь требует конфигурацию +func NewS3Storage(config S3Config) Storage { + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String(config.Region), + Endpoint: aws.String(config.Endpoint), + S3ForcePathStyle: aws.Bool(true), + Credentials: credentials.NewStaticCredentials( + config.AccessKey, + config.SecretKey, + "", + ), + })) + + uploader := s3manager.NewUploader(sess) + s3Client := s3.New(sess) + + return &s3Storage{ + uploader: uploader, + s3Client: s3Client, + bucket: config.Bucket, + region: config.Region, + endpoint: config.Endpoint, + } +} + +func (s *s3Storage) UploadVideo(ctx context.Context, videoData []byte, fileName string, userID int) (string, error) { + ext := strings.ToLower(filepath.Ext(fileName)) + timestamp := time.Now().UnixNano() + s3Key := fmt.Sprintf("clips/%d/videos/%d%s", userID, timestamp, ext) + + _, err := s.uploader.UploadWithContext(ctx, &s3manager.UploadInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s3Key), + Body: bytes.NewReader(videoData), + ContentType: aws.String(s.getVideoContentType(ext)), + }) + if err != nil { + return "", fmt.Errorf("failed to upload video to S3: %w", err) + } + + return fmt.Sprintf("%s/%s/%s", s.endpoint, s.bucket, s3Key), nil +} + +func (s *s3Storage) UploadThumbnail(ctx context.Context, thumbnailData []byte, userID int) (string, error) { + timestamp := time.Now().UnixNano() + s3Key := fmt.Sprintf("clips/%d/thumbnails/%d.jpg", userID, timestamp) + + _, err := s.uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s3Key), + Body: aws.ReadSeekCloser(strings.NewReader(string(thumbnailData))), + ContentType: aws.String("image/jpeg"), + }) + if err != nil { + return "", fmt.Errorf("failed to upload thumbnail to S3: %w", err) + } + + return fmt.Sprintf("%s/%s/%s", s.endpoint, s.bucket, s3Key), nil +} + +func (s *s3Storage) Delete(ctx context.Context, fileURL string) error { + s3Key, err := s.extractS3Key(fileURL) + if err != nil { + return err + } + + return s.deleteObject(s3Key) +} + +func (s *s3Storage) DeleteVideo(ctx context.Context, videoURL string) error { + s3Key, err := s.extractS3Key(videoURL) + if err != nil { + return err + } + + return s.deleteObject(s3Key) +} + +func (s *s3Storage) DeleteThumbnail(ctx context.Context, thumbnailURL string) error { + s3Key, err := s.extractS3Key(thumbnailURL) + if err != nil { + return err + } + + return s.deleteObject(s3Key) +} + +func (s *s3Storage) deleteObject(s3Key string) error { + _, err := s.s3Client.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s3Key), + }) + if err != nil { + return fmt.Errorf("failed to delete object from S3: %w", err) + } + + // Ждем пока объект действительно удалится + err = s.s3Client.WaitUntilObjectNotExists(&s3.HeadObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s3Key), + }) + if err != nil { + return fmt.Errorf("failed to wait for object deletion: %w", err) + } + + return nil +} + +func (s *s3Storage) extractS3Key(fileURL string) (string, error) { + // Убираем протокол и endpoint + cleanURL := strings.TrimPrefix(fileURL, s.endpoint+"/") + + // Убираем bucket name + parts := strings.SplitN(cleanURL, "/", 2) + if len(parts) != 2 { + return "", fmt.Errorf("invalid S3 URL format: %s", fileURL) + } + + if parts[0] != s.bucket { + return "", fmt.Errorf("invalid bucket in URL: expected %s, got %s", s.bucket, parts[0]) + } + + return parts[1], nil +} + +func (s *s3Storage) getVideoContentType(ext string) string { + switch ext { + case ".mp4": + return "video/mp4" + case ".webm": + return "video/webm" + case ".mov": + return "video/quicktime" + case ".avi": + return "video/x-msvideo" + case ".mkv": + return "video/x-matroska" + case ".flv": + return "video/x-flv" + case ".wmv": + return "video/x-ms-wmv" + default: + return "video/mp4" + } +} + +// BulkDelete удаляет несколько файлов +func (s *s3Storage) BulkDelete(ctx context.Context, fileURLs []string) error { + if len(fileURLs) == 0 { + return nil + } + + var objects []*s3.ObjectIdentifier + for _, url := range fileURLs { + s3Key, err := s.extractS3Key(url) + if err != nil { + return err + } + objects = append(objects, &s3.ObjectIdentifier{ + Key: aws.String(s3Key), + }) + } + + _, err := s.s3Client.DeleteObjects(&s3.DeleteObjectsInput{ + Bucket: aws.String(s.bucket), + Delete: &s3.Delete{ + Objects: objects, + Quiet: aws.Bool(false), + }, + }) + if err != nil { + return fmt.Errorf("failed to delete objects from S3: %w", err) + } + + return nil +} + +// CheckObjectExists проверяет существует ли объект в S3 +func (s *s3Storage) CheckObjectExists(ctx context.Context, fileURL string) (bool, error) { + s3Key, err := s.extractS3Key(fileURL) + if err != nil { + return false, err + } + + _, err = s.s3Client.HeadObject(&s3.HeadObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s3Key), + }) + if err != nil { + if strings.Contains(err.Error(), "NotFound") { + return false, nil + } + return false, fmt.Errorf("failed to check object existence: %w", err) + } + + return true, nil +} diff --git a/internal/utils/error_utils.go b/internal/utils/error_utils.go new file mode 100644 index 0000000..c45581d --- /dev/null +++ b/internal/utils/error_utils.go @@ -0,0 +1,16 @@ +package utils + +import ( + "strings" +) + +func IsDuplicateKeyError(err error) bool { + if err == nil { + return false + } + + // Для PostgreSQL + return strings.Contains(err.Error(), "duplicate key") || + strings.Contains(err.Error(), "violates unique constraint") + +} diff --git a/migrations/0001_initial_clips_schema.down.sql b/migrations/0001_initial_clips_schema.down.sql new file mode 100644 index 0000000..50dc73d --- /dev/null +++ b/migrations/0001_initial_clips_schema.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS clips CASCADE; +DROP TABLE IF EXISTS clip_likes CASCADE; +DROP TABLE IF EXISTS clip_comments CASCADE; \ No newline at end of file diff --git a/migrations/0001_initial_clips_schema.up.sql b/migrations/0001_initial_clips_schema.up.sql new file mode 100644 index 0000000..3432d4c --- /dev/null +++ b/migrations/0001_initial_clips_schema.up.sql @@ -0,0 +1,43 @@ +-- Клипы +CREATE TABLE clips ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + video_url TEXT NOT NULL, + thumbnail_url TEXT NOT NULL, + duration INT NOT NULL, + author_id INTEGER NOT NULL, + likes_count INTEGER DEFAULT 0, + comments_count INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMP WITH TIME ZONE +); + +-- Лайки клипов +CREATE TABLE clip_likes ( + id SERIAL PRIMARY KEY, + clip_id INTEGER NOT NULL REFERENCES clips(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + UNIQUE(clip_id, user_id) +); + +-- Комментарии к клипам +CREATE TABLE clip_comments ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL, + clip_id INTEGER NOT NULL REFERENCES clips(id) ON DELETE CASCADE, + author_id INTEGER NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMP WITH TIME ZONE +); + +-- Индексы для производительности +CREATE INDEX idx_clips_author_id ON clips(author_id); +CREATE INDEX idx_clip_likes_clip_id ON clip_likes(clip_id); +CREATE INDEX idx_clip_likes_user_id ON clip_likes(user_id); +CREATE INDEX idx_clip_comments_clip_id ON clip_comments(clip_id); +CREATE INDEX idx_clip_comments_author_id ON clip_comments(author_id); +CREATE INDEX idx_clips_deleted_at ON clips(deleted_at); +CREATE INDEX idx_clip_comments_deleted_at ON clip_comments(deleted_at); \ No newline at end of file diff --git a/proto/clip.pb.go b/proto/clip.pb.go new file mode 100644 index 0000000..85ff12d --- /dev/null +++ b/proto/clip.pb.go @@ -0,0 +1,1625 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc v3.21.12 +// source: clip.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateClipRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int32 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + VideoData []byte `protobuf:"bytes,3,opt,name=video_data,json=videoData,proto3" json:"video_data,omitempty"` + FileName string `protobuf:"bytes,4,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + ContentType string `protobuf:"bytes,5,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateClipRequest) Reset() { + *x = CreateClipRequest{} + mi := &file_clip_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateClipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateClipRequest) ProtoMessage() {} + +func (x *CreateClipRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateClipRequest.ProtoReflect.Descriptor instead. +func (*CreateClipRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateClipRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *CreateClipRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *CreateClipRequest) GetVideoData() []byte { + if x != nil { + return x.VideoData + } + return nil +} + +func (x *CreateClipRequest) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *CreateClipRequest) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +type CreateClipResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Clip *Clip `protobuf:"bytes,1,opt,name=clip,proto3" json:"clip,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateClipResponse) Reset() { + *x = CreateClipResponse{} + mi := &file_clip_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateClipResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateClipResponse) ProtoMessage() {} + +func (x *CreateClipResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateClipResponse.ProtoReflect.Descriptor instead. +func (*CreateClipResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateClipResponse) GetClip() *Clip { + if x != nil { + return x.Clip + } + return nil +} + +type GetClipRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClipId int32 `protobuf:"varint,1,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClipRequest) Reset() { + *x = GetClipRequest{} + mi := &file_clip_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClipRequest) ProtoMessage() {} + +func (x *GetClipRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClipRequest.ProtoReflect.Descriptor instead. +func (*GetClipRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{2} +} + +func (x *GetClipRequest) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +type GetClipResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Clip *Clip `protobuf:"bytes,1,opt,name=clip,proto3" json:"clip,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClipResponse) Reset() { + *x = GetClipResponse{} + mi := &file_clip_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClipResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClipResponse) ProtoMessage() {} + +func (x *GetClipResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClipResponse.ProtoReflect.Descriptor instead. +func (*GetClipResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{3} +} + +func (x *GetClipResponse) GetClip() *Clip { + if x != nil { + return x.Clip + } + return nil +} + +type GetUserClipsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int32 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserClipsRequest) Reset() { + *x = GetUserClipsRequest{} + mi := &file_clip_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserClipsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserClipsRequest) ProtoMessage() {} + +func (x *GetUserClipsRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserClipsRequest.ProtoReflect.Descriptor instead. +func (*GetUserClipsRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{4} +} + +func (x *GetUserClipsRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *GetUserClipsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *GetUserClipsRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +type GetUserClipsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Clips []*Clip `protobuf:"bytes,1,rep,name=clips,proto3" json:"clips,omitempty"` + TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserClipsResponse) Reset() { + *x = GetUserClipsResponse{} + mi := &file_clip_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserClipsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserClipsResponse) ProtoMessage() {} + +func (x *GetUserClipsResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserClipsResponse.ProtoReflect.Descriptor instead. +func (*GetUserClipsResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{5} +} + +func (x *GetUserClipsResponse) GetClips() []*Clip { + if x != nil { + return x.Clips + } + return nil +} + +func (x *GetUserClipsResponse) GetTotalCount() int32 { + if x != nil { + return x.TotalCount + } + return 0 +} + +type GetClipsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit int32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClipsRequest) Reset() { + *x = GetClipsRequest{} + mi := &file_clip_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClipsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClipsRequest) ProtoMessage() {} + +func (x *GetClipsRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClipsRequest.ProtoReflect.Descriptor instead. +func (*GetClipsRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{6} +} + +func (x *GetClipsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *GetClipsRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +type GetClipsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Clips []*Clip `protobuf:"bytes,1,rep,name=clips,proto3" json:"clips,omitempty"` + TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClipsResponse) Reset() { + *x = GetClipsResponse{} + mi := &file_clip_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClipsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClipsResponse) ProtoMessage() {} + +func (x *GetClipsResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClipsResponse.ProtoReflect.Descriptor instead. +func (*GetClipsResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{7} +} + +func (x *GetClipsResponse) GetClips() []*Clip { + if x != nil { + return x.Clips + } + return nil +} + +func (x *GetClipsResponse) GetTotalCount() int32 { + if x != nil { + return x.TotalCount + } + return 0 +} + +type DeleteClipRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClipId int32 `protobuf:"varint,1,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + UserId int32 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteClipRequest) Reset() { + *x = DeleteClipRequest{} + mi := &file_clip_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteClipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteClipRequest) ProtoMessage() {} + +func (x *DeleteClipRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteClipRequest.ProtoReflect.Descriptor instead. +func (*DeleteClipRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteClipRequest) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *DeleteClipRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +type LikeClipRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClipId int32 `protobuf:"varint,1,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + UserId int32 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LikeClipRequest) Reset() { + *x = LikeClipRequest{} + mi := &file_clip_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LikeClipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LikeClipRequest) ProtoMessage() {} + +func (x *LikeClipRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LikeClipRequest.ProtoReflect.Descriptor instead. +func (*LikeClipRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{9} +} + +func (x *LikeClipRequest) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *LikeClipRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +type LikeClipResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Like *ClipLike `protobuf:"bytes,1,opt,name=like,proto3" json:"like,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LikeClipResponse) Reset() { + *x = LikeClipResponse{} + mi := &file_clip_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LikeClipResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LikeClipResponse) ProtoMessage() {} + +func (x *LikeClipResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LikeClipResponse.ProtoReflect.Descriptor instead. +func (*LikeClipResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{10} +} + +func (x *LikeClipResponse) GetLike() *ClipLike { + if x != nil { + return x.Like + } + return nil +} + +type UnlikeClipRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClipId int32 `protobuf:"varint,1,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + UserId int32 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnlikeClipRequest) Reset() { + *x = UnlikeClipRequest{} + mi := &file_clip_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnlikeClipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnlikeClipRequest) ProtoMessage() {} + +func (x *UnlikeClipRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnlikeClipRequest.ProtoReflect.Descriptor instead. +func (*UnlikeClipRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{11} +} + +func (x *UnlikeClipRequest) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *UnlikeClipRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +type GetClipLikesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClipId int32 `protobuf:"varint,1,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClipLikesRequest) Reset() { + *x = GetClipLikesRequest{} + mi := &file_clip_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClipLikesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClipLikesRequest) ProtoMessage() {} + +func (x *GetClipLikesRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClipLikesRequest.ProtoReflect.Descriptor instead. +func (*GetClipLikesRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{12} +} + +func (x *GetClipLikesRequest) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *GetClipLikesRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *GetClipLikesRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +type GetClipLikesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Likes []*ClipLike `protobuf:"bytes,1,rep,name=likes,proto3" json:"likes,omitempty"` + TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClipLikesResponse) Reset() { + *x = GetClipLikesResponse{} + mi := &file_clip_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClipLikesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClipLikesResponse) ProtoMessage() {} + +func (x *GetClipLikesResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClipLikesResponse.ProtoReflect.Descriptor instead. +func (*GetClipLikesResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{13} +} + +func (x *GetClipLikesResponse) GetLikes() []*ClipLike { + if x != nil { + return x.Likes + } + return nil +} + +func (x *GetClipLikesResponse) GetTotalCount() int32 { + if x != nil { + return x.TotalCount + } + return 0 +} + +type CheckIfLikedRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClipId int32 `protobuf:"varint,1,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + UserId int32 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckIfLikedRequest) Reset() { + *x = CheckIfLikedRequest{} + mi := &file_clip_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckIfLikedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckIfLikedRequest) ProtoMessage() {} + +func (x *CheckIfLikedRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckIfLikedRequest.ProtoReflect.Descriptor instead. +func (*CheckIfLikedRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{14} +} + +func (x *CheckIfLikedRequest) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *CheckIfLikedRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +type CheckIfLikedResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + IsLiked bool `protobuf:"varint,1,opt,name=is_liked,json=isLiked,proto3" json:"is_liked,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckIfLikedResponse) Reset() { + *x = CheckIfLikedResponse{} + mi := &file_clip_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckIfLikedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckIfLikedResponse) ProtoMessage() {} + +func (x *CheckIfLikedResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckIfLikedResponse.ProtoReflect.Descriptor instead. +func (*CheckIfLikedResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{15} +} + +func (x *CheckIfLikedResponse) GetIsLiked() bool { + if x != nil { + return x.IsLiked + } + return false +} + +type CreateCommentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClipId int32 `protobuf:"varint,1,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + UserId int32 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateCommentRequest) Reset() { + *x = CreateCommentRequest{} + mi := &file_clip_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateCommentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateCommentRequest) ProtoMessage() {} + +func (x *CreateCommentRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateCommentRequest.ProtoReflect.Descriptor instead. +func (*CreateCommentRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{16} +} + +func (x *CreateCommentRequest) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *CreateCommentRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *CreateCommentRequest) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type CreateCommentResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Comment *ClipComment `protobuf:"bytes,1,opt,name=comment,proto3" json:"comment,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateCommentResponse) Reset() { + *x = CreateCommentResponse{} + mi := &file_clip_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateCommentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateCommentResponse) ProtoMessage() {} + +func (x *CreateCommentResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateCommentResponse.ProtoReflect.Descriptor instead. +func (*CreateCommentResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{17} +} + +func (x *CreateCommentResponse) GetComment() *ClipComment { + if x != nil { + return x.Comment + } + return nil +} + +type GetClipCommentsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClipId int32 `protobuf:"varint,1,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClipCommentsRequest) Reset() { + *x = GetClipCommentsRequest{} + mi := &file_clip_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClipCommentsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClipCommentsRequest) ProtoMessage() {} + +func (x *GetClipCommentsRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClipCommentsRequest.ProtoReflect.Descriptor instead. +func (*GetClipCommentsRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{18} +} + +func (x *GetClipCommentsRequest) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *GetClipCommentsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *GetClipCommentsRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +type GetClipCommentsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Comments []*ClipComment `protobuf:"bytes,1,rep,name=comments,proto3" json:"comments,omitempty"` + TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClipCommentsResponse) Reset() { + *x = GetClipCommentsResponse{} + mi := &file_clip_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClipCommentsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClipCommentsResponse) ProtoMessage() {} + +func (x *GetClipCommentsResponse) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClipCommentsResponse.ProtoReflect.Descriptor instead. +func (*GetClipCommentsResponse) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{19} +} + +func (x *GetClipCommentsResponse) GetComments() []*ClipComment { + if x != nil { + return x.Comments + } + return nil +} + +func (x *GetClipCommentsResponse) GetTotalCount() int32 { + if x != nil { + return x.TotalCount + } + return 0 +} + +type DeleteCommentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + CommentId int32 `protobuf:"varint,1,opt,name=comment_id,json=commentId,proto3" json:"comment_id,omitempty"` + UserId int32 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteCommentRequest) Reset() { + *x = DeleteCommentRequest{} + mi := &file_clip_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteCommentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteCommentRequest) ProtoMessage() {} + +func (x *DeleteCommentRequest) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteCommentRequest.ProtoReflect.Descriptor instead. +func (*DeleteCommentRequest) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{20} +} + +func (x *DeleteCommentRequest) GetCommentId() int32 { + if x != nil { + return x.CommentId + } + return 0 +} + +func (x *DeleteCommentRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +type Clip struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + VideoUrl string `protobuf:"bytes,3,opt,name=video_url,json=videoUrl,proto3" json:"video_url,omitempty"` + ThumbnailUrl string `protobuf:"bytes,4,opt,name=thumbnail_url,json=thumbnailUrl,proto3" json:"thumbnail_url,omitempty"` + Duration int32 `protobuf:"varint,5,opt,name=duration,proto3" json:"duration,omitempty"` + AuthorId int32 `protobuf:"varint,6,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` + LikesCount int32 `protobuf:"varint,7,opt,name=likes_count,json=likesCount,proto3" json:"likes_count,omitempty"` + CommentsCount int32 `protobuf:"varint,8,opt,name=comments_count,json=commentsCount,proto3" json:"comments_count,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Clip) Reset() { + *x = Clip{} + mi := &file_clip_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Clip) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Clip) ProtoMessage() {} + +func (x *Clip) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Clip.ProtoReflect.Descriptor instead. +func (*Clip) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{21} +} + +func (x *Clip) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Clip) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Clip) GetVideoUrl() string { + if x != nil { + return x.VideoUrl + } + return "" +} + +func (x *Clip) GetThumbnailUrl() string { + if x != nil { + return x.ThumbnailUrl + } + return "" +} + +func (x *Clip) GetDuration() int32 { + if x != nil { + return x.Duration + } + return 0 +} + +func (x *Clip) GetAuthorId() int32 { + if x != nil { + return x.AuthorId + } + return 0 +} + +func (x *Clip) GetLikesCount() int32 { + if x != nil { + return x.LikesCount + } + return 0 +} + +func (x *Clip) GetCommentsCount() int32 { + if x != nil { + return x.CommentsCount + } + return 0 +} + +func (x *Clip) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Clip) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type ClipLike struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + ClipId int32 `protobuf:"varint,2,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + UserId int32 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClipLike) Reset() { + *x = ClipLike{} + mi := &file_clip_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClipLike) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClipLike) ProtoMessage() {} + +func (x *ClipLike) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClipLike.ProtoReflect.Descriptor instead. +func (*ClipLike) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{22} +} + +func (x *ClipLike) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ClipLike) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *ClipLike) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *ClipLike) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type ClipComment struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + ClipId int32 `protobuf:"varint,2,opt,name=clip_id,json=clipId,proto3" json:"clip_id,omitempty"` + AuthorId int32 `protobuf:"varint,3,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` + Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClipComment) Reset() { + *x = ClipComment{} + mi := &file_clip_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClipComment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClipComment) ProtoMessage() {} + +func (x *ClipComment) ProtoReflect() protoreflect.Message { + mi := &file_clip_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClipComment.ProtoReflect.Descriptor instead. +func (*ClipComment) Descriptor() ([]byte, []int) { + return file_clip_proto_rawDescGZIP(), []int{23} +} + +func (x *ClipComment) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ClipComment) GetClipId() int32 { + if x != nil { + return x.ClipId + } + return 0 +} + +func (x *ClipComment) GetAuthorId() int32 { + if x != nil { + return x.AuthorId + } + return 0 +} + +func (x *ClipComment) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *ClipComment) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ClipComment) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +var File_clip_proto protoreflect.FileDescriptor + +const file_clip_proto_rawDesc = "" + + "\n" + + "\n" + + "clip.proto\x12\x05proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xa1\x01\n" + + "\x11CreateClipRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x05R\x06userId\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1d\n" + + "\n" + + "video_data\x18\x03 \x01(\fR\tvideoData\x12\x1b\n" + + "\tfile_name\x18\x04 \x01(\tR\bfileName\x12!\n" + + "\fcontent_type\x18\x05 \x01(\tR\vcontentType\"5\n" + + "\x12CreateClipResponse\x12\x1f\n" + + "\x04clip\x18\x01 \x01(\v2\v.proto.ClipR\x04clip\")\n" + + "\x0eGetClipRequest\x12\x17\n" + + "\aclip_id\x18\x01 \x01(\x05R\x06clipId\"2\n" + + "\x0fGetClipResponse\x12\x1f\n" + + "\x04clip\x18\x01 \x01(\v2\v.proto.ClipR\x04clip\"\\\n" + + "\x13GetUserClipsRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x05R\x06userId\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\x12\x16\n" + + "\x06offset\x18\x03 \x01(\x05R\x06offset\"Z\n" + + "\x14GetUserClipsResponse\x12!\n" + + "\x05clips\x18\x01 \x03(\v2\v.proto.ClipR\x05clips\x12\x1f\n" + + "\vtotal_count\x18\x02 \x01(\x05R\n" + + "totalCount\"?\n" + + "\x0fGetClipsRequest\x12\x14\n" + + "\x05limit\x18\x01 \x01(\x05R\x05limit\x12\x16\n" + + "\x06offset\x18\x02 \x01(\x05R\x06offset\"V\n" + + "\x10GetClipsResponse\x12!\n" + + "\x05clips\x18\x01 \x03(\v2\v.proto.ClipR\x05clips\x12\x1f\n" + + "\vtotal_count\x18\x02 \x01(\x05R\n" + + "totalCount\"E\n" + + "\x11DeleteClipRequest\x12\x17\n" + + "\aclip_id\x18\x01 \x01(\x05R\x06clipId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x05R\x06userId\"C\n" + + "\x0fLikeClipRequest\x12\x17\n" + + "\aclip_id\x18\x01 \x01(\x05R\x06clipId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x05R\x06userId\"7\n" + + "\x10LikeClipResponse\x12#\n" + + "\x04like\x18\x01 \x01(\v2\x0f.proto.ClipLikeR\x04like\"E\n" + + "\x11UnlikeClipRequest\x12\x17\n" + + "\aclip_id\x18\x01 \x01(\x05R\x06clipId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x05R\x06userId\"\\\n" + + "\x13GetClipLikesRequest\x12\x17\n" + + "\aclip_id\x18\x01 \x01(\x05R\x06clipId\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\x12\x16\n" + + "\x06offset\x18\x03 \x01(\x05R\x06offset\"^\n" + + "\x14GetClipLikesResponse\x12%\n" + + "\x05likes\x18\x01 \x03(\v2\x0f.proto.ClipLikeR\x05likes\x12\x1f\n" + + "\vtotal_count\x18\x02 \x01(\x05R\n" + + "totalCount\"G\n" + + "\x13CheckIfLikedRequest\x12\x17\n" + + "\aclip_id\x18\x01 \x01(\x05R\x06clipId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x05R\x06userId\"1\n" + + "\x14CheckIfLikedResponse\x12\x19\n" + + "\bis_liked\x18\x01 \x01(\bR\aisLiked\"b\n" + + "\x14CreateCommentRequest\x12\x17\n" + + "\aclip_id\x18\x01 \x01(\x05R\x06clipId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x05R\x06userId\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\"E\n" + + "\x15CreateCommentResponse\x12,\n" + + "\acomment\x18\x01 \x01(\v2\x12.proto.ClipCommentR\acomment\"_\n" + + "\x16GetClipCommentsRequest\x12\x17\n" + + "\aclip_id\x18\x01 \x01(\x05R\x06clipId\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\x12\x16\n" + + "\x06offset\x18\x03 \x01(\x05R\x06offset\"j\n" + + "\x17GetClipCommentsResponse\x12.\n" + + "\bcomments\x18\x01 \x03(\v2\x12.proto.ClipCommentR\bcomments\x12\x1f\n" + + "\vtotal_count\x18\x02 \x01(\x05R\n" + + "totalCount\"N\n" + + "\x14DeleteCommentRequest\x12\x1d\n" + + "\n" + + "comment_id\x18\x01 \x01(\x05R\tcommentId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x05R\x06userId\"\xe5\x02\n" + + "\x04Clip\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1b\n" + + "\tvideo_url\x18\x03 \x01(\tR\bvideoUrl\x12#\n" + + "\rthumbnail_url\x18\x04 \x01(\tR\fthumbnailUrl\x12\x1a\n" + + "\bduration\x18\x05 \x01(\x05R\bduration\x12\x1b\n" + + "\tauthor_id\x18\x06 \x01(\x05R\bauthorId\x12\x1f\n" + + "\vlikes_count\x18\a \x01(\x05R\n" + + "likesCount\x12%\n" + + "\x0ecomments_count\x18\b \x01(\x05R\rcommentsCount\x129\n" + + "\n" + + "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + + "\n" + + "updated_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x87\x01\n" + + "\bClipLike\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x17\n" + + "\aclip_id\x18\x02 \x01(\x05R\x06clipId\x12\x17\n" + + "\auser_id\x18\x03 \x01(\x05R\x06userId\x129\n" + + "\n" + + "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xe3\x01\n" + + "\vClipComment\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x17\n" + + "\aclip_id\x18\x02 \x01(\x05R\x06clipId\x12\x1b\n" + + "\tauthor_id\x18\x03 \x01(\x05R\bauthorId\x12\x18\n" + + "\acontent\x18\x04 \x01(\tR\acontent\x129\n" + + "\n" + + "created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + + "\n" + + "updated_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt2\xc3\x06\n" + + "\vClipService\x12A\n" + + "\n" + + "CreateClip\x12\x18.proto.CreateClipRequest\x1a\x19.proto.CreateClipResponse\x128\n" + + "\aGetClip\x12\x15.proto.GetClipRequest\x1a\x16.proto.GetClipResponse\x12G\n" + + "\fGetUserClips\x12\x1a.proto.GetUserClipsRequest\x1a\x1b.proto.GetUserClipsResponse\x12;\n" + + "\bGetClips\x12\x16.proto.GetClipsRequest\x1a\x17.proto.GetClipsResponse\x12>\n" + + "\n" + + "DeleteClip\x12\x18.proto.DeleteClipRequest\x1a\x16.google.protobuf.Empty\x12;\n" + + "\bLikeClip\x12\x16.proto.LikeClipRequest\x1a\x17.proto.LikeClipResponse\x12>\n" + + "\n" + + "UnlikeClip\x12\x18.proto.UnlikeClipRequest\x1a\x16.google.protobuf.Empty\x12G\n" + + "\fGetClipLikes\x12\x1a.proto.GetClipLikesRequest\x1a\x1b.proto.GetClipLikesResponse\x12G\n" + + "\fCheckIfLiked\x12\x1a.proto.CheckIfLikedRequest\x1a\x1b.proto.CheckIfLikedResponse\x12J\n" + + "\rCreateComment\x12\x1b.proto.CreateCommentRequest\x1a\x1c.proto.CreateCommentResponse\x12P\n" + + "\x0fGetClipComments\x12\x1d.proto.GetClipCommentsRequest\x1a\x1e.proto.GetClipCommentsResponse\x12D\n" + + "\rDeleteComment\x12\x1b.proto.DeleteCommentRequest\x1a\x16.google.protobuf.EmptyB\tZ\a.;protob\x06proto3" + +var ( + file_clip_proto_rawDescOnce sync.Once + file_clip_proto_rawDescData []byte +) + +func file_clip_proto_rawDescGZIP() []byte { + file_clip_proto_rawDescOnce.Do(func() { + file_clip_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_clip_proto_rawDesc), len(file_clip_proto_rawDesc))) + }) + return file_clip_proto_rawDescData +} + +var file_clip_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_clip_proto_goTypes = []any{ + (*CreateClipRequest)(nil), // 0: proto.CreateClipRequest + (*CreateClipResponse)(nil), // 1: proto.CreateClipResponse + (*GetClipRequest)(nil), // 2: proto.GetClipRequest + (*GetClipResponse)(nil), // 3: proto.GetClipResponse + (*GetUserClipsRequest)(nil), // 4: proto.GetUserClipsRequest + (*GetUserClipsResponse)(nil), // 5: proto.GetUserClipsResponse + (*GetClipsRequest)(nil), // 6: proto.GetClipsRequest + (*GetClipsResponse)(nil), // 7: proto.GetClipsResponse + (*DeleteClipRequest)(nil), // 8: proto.DeleteClipRequest + (*LikeClipRequest)(nil), // 9: proto.LikeClipRequest + (*LikeClipResponse)(nil), // 10: proto.LikeClipResponse + (*UnlikeClipRequest)(nil), // 11: proto.UnlikeClipRequest + (*GetClipLikesRequest)(nil), // 12: proto.GetClipLikesRequest + (*GetClipLikesResponse)(nil), // 13: proto.GetClipLikesResponse + (*CheckIfLikedRequest)(nil), // 14: proto.CheckIfLikedRequest + (*CheckIfLikedResponse)(nil), // 15: proto.CheckIfLikedResponse + (*CreateCommentRequest)(nil), // 16: proto.CreateCommentRequest + (*CreateCommentResponse)(nil), // 17: proto.CreateCommentResponse + (*GetClipCommentsRequest)(nil), // 18: proto.GetClipCommentsRequest + (*GetClipCommentsResponse)(nil), // 19: proto.GetClipCommentsResponse + (*DeleteCommentRequest)(nil), // 20: proto.DeleteCommentRequest + (*Clip)(nil), // 21: proto.Clip + (*ClipLike)(nil), // 22: proto.ClipLike + (*ClipComment)(nil), // 23: proto.ClipComment + (*timestamppb.Timestamp)(nil), // 24: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 25: google.protobuf.Empty +} +var file_clip_proto_depIdxs = []int32{ + 21, // 0: proto.CreateClipResponse.clip:type_name -> proto.Clip + 21, // 1: proto.GetClipResponse.clip:type_name -> proto.Clip + 21, // 2: proto.GetUserClipsResponse.clips:type_name -> proto.Clip + 21, // 3: proto.GetClipsResponse.clips:type_name -> proto.Clip + 22, // 4: proto.LikeClipResponse.like:type_name -> proto.ClipLike + 22, // 5: proto.GetClipLikesResponse.likes:type_name -> proto.ClipLike + 23, // 6: proto.CreateCommentResponse.comment:type_name -> proto.ClipComment + 23, // 7: proto.GetClipCommentsResponse.comments:type_name -> proto.ClipComment + 24, // 8: proto.Clip.created_at:type_name -> google.protobuf.Timestamp + 24, // 9: proto.Clip.updated_at:type_name -> google.protobuf.Timestamp + 24, // 10: proto.ClipLike.created_at:type_name -> google.protobuf.Timestamp + 24, // 11: proto.ClipComment.created_at:type_name -> google.protobuf.Timestamp + 24, // 12: proto.ClipComment.updated_at:type_name -> google.protobuf.Timestamp + 0, // 13: proto.ClipService.CreateClip:input_type -> proto.CreateClipRequest + 2, // 14: proto.ClipService.GetClip:input_type -> proto.GetClipRequest + 4, // 15: proto.ClipService.GetUserClips:input_type -> proto.GetUserClipsRequest + 6, // 16: proto.ClipService.GetClips:input_type -> proto.GetClipsRequest + 8, // 17: proto.ClipService.DeleteClip:input_type -> proto.DeleteClipRequest + 9, // 18: proto.ClipService.LikeClip:input_type -> proto.LikeClipRequest + 11, // 19: proto.ClipService.UnlikeClip:input_type -> proto.UnlikeClipRequest + 12, // 20: proto.ClipService.GetClipLikes:input_type -> proto.GetClipLikesRequest + 14, // 21: proto.ClipService.CheckIfLiked:input_type -> proto.CheckIfLikedRequest + 16, // 22: proto.ClipService.CreateComment:input_type -> proto.CreateCommentRequest + 18, // 23: proto.ClipService.GetClipComments:input_type -> proto.GetClipCommentsRequest + 20, // 24: proto.ClipService.DeleteComment:input_type -> proto.DeleteCommentRequest + 1, // 25: proto.ClipService.CreateClip:output_type -> proto.CreateClipResponse + 3, // 26: proto.ClipService.GetClip:output_type -> proto.GetClipResponse + 5, // 27: proto.ClipService.GetUserClips:output_type -> proto.GetUserClipsResponse + 7, // 28: proto.ClipService.GetClips:output_type -> proto.GetClipsResponse + 25, // 29: proto.ClipService.DeleteClip:output_type -> google.protobuf.Empty + 10, // 30: proto.ClipService.LikeClip:output_type -> proto.LikeClipResponse + 25, // 31: proto.ClipService.UnlikeClip:output_type -> google.protobuf.Empty + 13, // 32: proto.ClipService.GetClipLikes:output_type -> proto.GetClipLikesResponse + 15, // 33: proto.ClipService.CheckIfLiked:output_type -> proto.CheckIfLikedResponse + 17, // 34: proto.ClipService.CreateComment:output_type -> proto.CreateCommentResponse + 19, // 35: proto.ClipService.GetClipComments:output_type -> proto.GetClipCommentsResponse + 25, // 36: proto.ClipService.DeleteComment:output_type -> google.protobuf.Empty + 25, // [25:37] is the sub-list for method output_type + 13, // [13:25] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_clip_proto_init() } +func file_clip_proto_init() { + if File_clip_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_clip_proto_rawDesc), len(file_clip_proto_rawDesc)), + NumEnums: 0, + NumMessages: 24, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_clip_proto_goTypes, + DependencyIndexes: file_clip_proto_depIdxs, + MessageInfos: file_clip_proto_msgTypes, + }.Build() + File_clip_proto = out.File + file_clip_proto_goTypes = nil + file_clip_proto_depIdxs = nil +} diff --git a/proto/clip.proto b/proto/clip.proto new file mode 100644 index 0000000..31535f7 --- /dev/null +++ b/proto/clip.proto @@ -0,0 +1,163 @@ +syntax = "proto3"; + +package proto; + +option go_package = ".;proto"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +service ClipService { + // Клипы + rpc CreateClip(CreateClipRequest) returns (CreateClipResponse); + rpc GetClip(GetClipRequest) returns (GetClipResponse); + rpc GetUserClips(GetUserClipsRequest) returns (GetUserClipsResponse); + rpc GetClips(GetClipsRequest) returns (GetClipsResponse); + rpc DeleteClip(DeleteClipRequest) returns (google.protobuf.Empty); + + // Лайки + rpc LikeClip(LikeClipRequest) returns (LikeClipResponse); + rpc UnlikeClip(UnlikeClipRequest) returns (google.protobuf.Empty); + rpc GetClipLikes(GetClipLikesRequest) returns (GetClipLikesResponse); + rpc CheckIfLiked(CheckIfLikedRequest) returns (CheckIfLikedResponse); + + // Комментарии + rpc CreateComment(CreateCommentRequest) returns (CreateCommentResponse); + rpc GetClipComments(GetClipCommentsRequest) returns (GetClipCommentsResponse); + rpc DeleteComment(DeleteCommentRequest) returns (google.protobuf.Empty); +} + +message CreateClipRequest { + int32 user_id = 1; + string title = 2; + bytes video_data = 3; + string file_name = 4; + string content_type = 5; +} + +message CreateClipResponse { + Clip clip = 1; +} + +message GetClipRequest { + int32 clip_id = 1; +} + +message GetClipResponse { + Clip clip = 1; +} + +message GetUserClipsRequest { + int32 user_id = 1; + int32 limit = 2; + int32 offset = 3; +} + +message GetUserClipsResponse { + repeated Clip clips = 1; + int32 total_count = 2; +} + +message GetClipsRequest { + int32 limit = 1; + int32 offset = 2; +} + +message GetClipsResponse { + repeated Clip clips = 1; + int32 total_count = 2; +} + +message DeleteClipRequest { + int32 clip_id = 1; + int32 user_id = 2; +} + +message LikeClipRequest { + int32 clip_id = 1; + int32 user_id = 2; +} + +message LikeClipResponse { + ClipLike like = 1; +} + +message UnlikeClipRequest { + int32 clip_id = 1; + int32 user_id = 2; +} + +message GetClipLikesRequest { + int32 clip_id = 1; + int32 limit = 2; + int32 offset = 3; +} + +message GetClipLikesResponse { + repeated ClipLike likes = 1; + int32 total_count = 2; +} + +message CheckIfLikedRequest { + int32 clip_id = 1; + int32 user_id = 2; +} + +message CheckIfLikedResponse { + bool is_liked = 1; +} + +message CreateCommentRequest { + int32 clip_id = 1; + int32 user_id = 2; + string content = 3; +} + +message CreateCommentResponse { + ClipComment comment = 1; +} + +message GetClipCommentsRequest { + int32 clip_id = 1; + int32 limit = 2; + int32 offset = 3; +} + +message GetClipCommentsResponse { + repeated ClipComment comments = 1; + int32 total_count = 2; +} + +message DeleteCommentRequest { + int32 comment_id = 1; + int32 user_id = 2; +} + +message Clip { + int32 id = 1; + string title = 2; + string video_url = 3; + string thumbnail_url = 4; + int32 duration = 5; + int32 author_id = 6; + int32 likes_count = 7; + int32 comments_count = 8; + google.protobuf.Timestamp created_at = 9; + google.protobuf.Timestamp updated_at = 10; +} + +message ClipLike { + int32 id = 1; + int32 clip_id = 2; + int32 user_id = 3; + google.protobuf.Timestamp created_at = 4; +} + +message ClipComment { + int32 id = 1; + int32 clip_id = 2; + int32 author_id = 3; + string content = 4; + google.protobuf.Timestamp created_at = 5; + google.protobuf.Timestamp updated_at = 6; +} \ No newline at end of file diff --git a/proto/clip_grpc.pb.go b/proto/clip_grpc.pb.go new file mode 100644 index 0000000..e803cd3 --- /dev/null +++ b/proto/clip_grpc.pb.go @@ -0,0 +1,546 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v3.21.12 +// source: clip.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ClipService_CreateClip_FullMethodName = "/proto.ClipService/CreateClip" + ClipService_GetClip_FullMethodName = "/proto.ClipService/GetClip" + ClipService_GetUserClips_FullMethodName = "/proto.ClipService/GetUserClips" + ClipService_GetClips_FullMethodName = "/proto.ClipService/GetClips" + ClipService_DeleteClip_FullMethodName = "/proto.ClipService/DeleteClip" + ClipService_LikeClip_FullMethodName = "/proto.ClipService/LikeClip" + ClipService_UnlikeClip_FullMethodName = "/proto.ClipService/UnlikeClip" + ClipService_GetClipLikes_FullMethodName = "/proto.ClipService/GetClipLikes" + ClipService_CheckIfLiked_FullMethodName = "/proto.ClipService/CheckIfLiked" + ClipService_CreateComment_FullMethodName = "/proto.ClipService/CreateComment" + ClipService_GetClipComments_FullMethodName = "/proto.ClipService/GetClipComments" + ClipService_DeleteComment_FullMethodName = "/proto.ClipService/DeleteComment" +) + +// ClipServiceClient is the client API for ClipService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ClipServiceClient interface { + // Клипы + CreateClip(ctx context.Context, in *CreateClipRequest, opts ...grpc.CallOption) (*CreateClipResponse, error) + GetClip(ctx context.Context, in *GetClipRequest, opts ...grpc.CallOption) (*GetClipResponse, error) + GetUserClips(ctx context.Context, in *GetUserClipsRequest, opts ...grpc.CallOption) (*GetUserClipsResponse, error) + GetClips(ctx context.Context, in *GetClipsRequest, opts ...grpc.CallOption) (*GetClipsResponse, error) + DeleteClip(ctx context.Context, in *DeleteClipRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Лайки + LikeClip(ctx context.Context, in *LikeClipRequest, opts ...grpc.CallOption) (*LikeClipResponse, error) + UnlikeClip(ctx context.Context, in *UnlikeClipRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetClipLikes(ctx context.Context, in *GetClipLikesRequest, opts ...grpc.CallOption) (*GetClipLikesResponse, error) + CheckIfLiked(ctx context.Context, in *CheckIfLikedRequest, opts ...grpc.CallOption) (*CheckIfLikedResponse, error) + // Комментарии + CreateComment(ctx context.Context, in *CreateCommentRequest, opts ...grpc.CallOption) (*CreateCommentResponse, error) + GetClipComments(ctx context.Context, in *GetClipCommentsRequest, opts ...grpc.CallOption) (*GetClipCommentsResponse, error) + DeleteComment(ctx context.Context, in *DeleteCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type clipServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewClipServiceClient(cc grpc.ClientConnInterface) ClipServiceClient { + return &clipServiceClient{cc} +} + +func (c *clipServiceClient) CreateClip(ctx context.Context, in *CreateClipRequest, opts ...grpc.CallOption) (*CreateClipResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateClipResponse) + err := c.cc.Invoke(ctx, ClipService_CreateClip_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) GetClip(ctx context.Context, in *GetClipRequest, opts ...grpc.CallOption) (*GetClipResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetClipResponse) + err := c.cc.Invoke(ctx, ClipService_GetClip_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) GetUserClips(ctx context.Context, in *GetUserClipsRequest, opts ...grpc.CallOption) (*GetUserClipsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetUserClipsResponse) + err := c.cc.Invoke(ctx, ClipService_GetUserClips_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) GetClips(ctx context.Context, in *GetClipsRequest, opts ...grpc.CallOption) (*GetClipsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetClipsResponse) + err := c.cc.Invoke(ctx, ClipService_GetClips_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) DeleteClip(ctx context.Context, in *DeleteClipRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ClipService_DeleteClip_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) LikeClip(ctx context.Context, in *LikeClipRequest, opts ...grpc.CallOption) (*LikeClipResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LikeClipResponse) + err := c.cc.Invoke(ctx, ClipService_LikeClip_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) UnlikeClip(ctx context.Context, in *UnlikeClipRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ClipService_UnlikeClip_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) GetClipLikes(ctx context.Context, in *GetClipLikesRequest, opts ...grpc.CallOption) (*GetClipLikesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetClipLikesResponse) + err := c.cc.Invoke(ctx, ClipService_GetClipLikes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) CheckIfLiked(ctx context.Context, in *CheckIfLikedRequest, opts ...grpc.CallOption) (*CheckIfLikedResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckIfLikedResponse) + err := c.cc.Invoke(ctx, ClipService_CheckIfLiked_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) CreateComment(ctx context.Context, in *CreateCommentRequest, opts ...grpc.CallOption) (*CreateCommentResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateCommentResponse) + err := c.cc.Invoke(ctx, ClipService_CreateComment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) GetClipComments(ctx context.Context, in *GetClipCommentsRequest, opts ...grpc.CallOption) (*GetClipCommentsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetClipCommentsResponse) + err := c.cc.Invoke(ctx, ClipService_GetClipComments_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clipServiceClient) DeleteComment(ctx context.Context, in *DeleteCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ClipService_DeleteComment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ClipServiceServer is the server API for ClipService service. +// All implementations must embed UnimplementedClipServiceServer +// for forward compatibility. +type ClipServiceServer interface { + // Клипы + CreateClip(context.Context, *CreateClipRequest) (*CreateClipResponse, error) + GetClip(context.Context, *GetClipRequest) (*GetClipResponse, error) + GetUserClips(context.Context, *GetUserClipsRequest) (*GetUserClipsResponse, error) + GetClips(context.Context, *GetClipsRequest) (*GetClipsResponse, error) + DeleteClip(context.Context, *DeleteClipRequest) (*emptypb.Empty, error) + // Лайки + LikeClip(context.Context, *LikeClipRequest) (*LikeClipResponse, error) + UnlikeClip(context.Context, *UnlikeClipRequest) (*emptypb.Empty, error) + GetClipLikes(context.Context, *GetClipLikesRequest) (*GetClipLikesResponse, error) + CheckIfLiked(context.Context, *CheckIfLikedRequest) (*CheckIfLikedResponse, error) + // Комментарии + CreateComment(context.Context, *CreateCommentRequest) (*CreateCommentResponse, error) + GetClipComments(context.Context, *GetClipCommentsRequest) (*GetClipCommentsResponse, error) + DeleteComment(context.Context, *DeleteCommentRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedClipServiceServer() +} + +// UnimplementedClipServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedClipServiceServer struct{} + +func (UnimplementedClipServiceServer) CreateClip(context.Context, *CreateClipRequest) (*CreateClipResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateClip not implemented") +} +func (UnimplementedClipServiceServer) GetClip(context.Context, *GetClipRequest) (*GetClipResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetClip not implemented") +} +func (UnimplementedClipServiceServer) GetUserClips(context.Context, *GetUserClipsRequest) (*GetUserClipsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUserClips not implemented") +} +func (UnimplementedClipServiceServer) GetClips(context.Context, *GetClipsRequest) (*GetClipsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetClips not implemented") +} +func (UnimplementedClipServiceServer) DeleteClip(context.Context, *DeleteClipRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteClip not implemented") +} +func (UnimplementedClipServiceServer) LikeClip(context.Context, *LikeClipRequest) (*LikeClipResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method LikeClip not implemented") +} +func (UnimplementedClipServiceServer) UnlikeClip(context.Context, *UnlikeClipRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnlikeClip not implemented") +} +func (UnimplementedClipServiceServer) GetClipLikes(context.Context, *GetClipLikesRequest) (*GetClipLikesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetClipLikes not implemented") +} +func (UnimplementedClipServiceServer) CheckIfLiked(context.Context, *CheckIfLikedRequest) (*CheckIfLikedResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckIfLiked not implemented") +} +func (UnimplementedClipServiceServer) CreateComment(context.Context, *CreateCommentRequest) (*CreateCommentResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateComment not implemented") +} +func (UnimplementedClipServiceServer) GetClipComments(context.Context, *GetClipCommentsRequest) (*GetClipCommentsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetClipComments not implemented") +} +func (UnimplementedClipServiceServer) DeleteComment(context.Context, *DeleteCommentRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteComment not implemented") +} +func (UnimplementedClipServiceServer) mustEmbedUnimplementedClipServiceServer() {} +func (UnimplementedClipServiceServer) testEmbeddedByValue() {} + +// UnsafeClipServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ClipServiceServer will +// result in compilation errors. +type UnsafeClipServiceServer interface { + mustEmbedUnimplementedClipServiceServer() +} + +func RegisterClipServiceServer(s grpc.ServiceRegistrar, srv ClipServiceServer) { + // If the following call pancis, it indicates UnimplementedClipServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ClipService_ServiceDesc, srv) +} + +func _ClipService_CreateClip_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateClipRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).CreateClip(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_CreateClip_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).CreateClip(ctx, req.(*CreateClipRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_GetClip_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetClipRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).GetClip(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_GetClip_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).GetClip(ctx, req.(*GetClipRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_GetUserClips_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserClipsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).GetUserClips(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_GetUserClips_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).GetUserClips(ctx, req.(*GetUserClipsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_GetClips_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetClipsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).GetClips(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_GetClips_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).GetClips(ctx, req.(*GetClipsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_DeleteClip_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteClipRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).DeleteClip(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_DeleteClip_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).DeleteClip(ctx, req.(*DeleteClipRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_LikeClip_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LikeClipRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).LikeClip(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_LikeClip_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).LikeClip(ctx, req.(*LikeClipRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_UnlikeClip_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnlikeClipRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).UnlikeClip(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_UnlikeClip_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).UnlikeClip(ctx, req.(*UnlikeClipRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_GetClipLikes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetClipLikesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).GetClipLikes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_GetClipLikes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).GetClipLikes(ctx, req.(*GetClipLikesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_CheckIfLiked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckIfLikedRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).CheckIfLiked(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_CheckIfLiked_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).CheckIfLiked(ctx, req.(*CheckIfLikedRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_CreateComment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateCommentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).CreateComment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_CreateComment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).CreateComment(ctx, req.(*CreateCommentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_GetClipComments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetClipCommentsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).GetClipComments(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_GetClipComments_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).GetClipComments(ctx, req.(*GetClipCommentsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClipService_DeleteComment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteCommentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClipServiceServer).DeleteComment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClipService_DeleteComment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClipServiceServer).DeleteComment(ctx, req.(*DeleteCommentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ClipService_ServiceDesc is the grpc.ServiceDesc for ClipService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ClipService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.ClipService", + HandlerType: (*ClipServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateClip", + Handler: _ClipService_CreateClip_Handler, + }, + { + MethodName: "GetClip", + Handler: _ClipService_GetClip_Handler, + }, + { + MethodName: "GetUserClips", + Handler: _ClipService_GetUserClips_Handler, + }, + { + MethodName: "GetClips", + Handler: _ClipService_GetClips_Handler, + }, + { + MethodName: "DeleteClip", + Handler: _ClipService_DeleteClip_Handler, + }, + { + MethodName: "LikeClip", + Handler: _ClipService_LikeClip_Handler, + }, + { + MethodName: "UnlikeClip", + Handler: _ClipService_UnlikeClip_Handler, + }, + { + MethodName: "GetClipLikes", + Handler: _ClipService_GetClipLikes_Handler, + }, + { + MethodName: "CheckIfLiked", + Handler: _ClipService_CheckIfLiked_Handler, + }, + { + MethodName: "CreateComment", + Handler: _ClipService_CreateComment_Handler, + }, + { + MethodName: "GetClipComments", + Handler: _ClipService_GetClipComments_Handler, + }, + { + MethodName: "DeleteComment", + Handler: _ClipService_DeleteComment_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "clip.proto", +}